mirror of
https://github.com/dnote/dnote
synced 2026-03-14 22:45:50 +01:00
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:
parent
cc27714fb6
commit
f526124243
16 changed files with 447 additions and 113 deletions
|
|
@ -38,7 +38,7 @@ dnote view
|
|||
dnote view golang
|
||||
|
||||
# See details of a note
|
||||
dnote view golang 12
|
||||
dnote view 12
|
||||
```
|
||||
|
||||
## dnote edit
|
||||
|
|
@ -48,11 +48,11 @@ _alias: e_
|
|||
Edit a note.
|
||||
|
||||
```bash
|
||||
# Launch a text editor to edit a note with the given index.
|
||||
dnote edit linux 1
|
||||
# Launch a text editor to edit a note with the given id.
|
||||
dnote edit 12
|
||||
|
||||
# Edit a note with the given index in the specified book with a content.
|
||||
dnote edit linux 1 -c "New Content"
|
||||
# Edit a note with the given id in the specified book with a content.
|
||||
dnote edit 12 -c "New Content"
|
||||
```
|
||||
|
||||
## dnote remove
|
||||
|
|
@ -62,8 +62,8 @@ _alias: d_
|
|||
Remove either a note or a book.
|
||||
|
||||
```bash
|
||||
# Remove the note with `index` in the specified book.
|
||||
dnote remove JS 1
|
||||
# Remove the note with an id in the specified book.
|
||||
dnote remove 1
|
||||
|
||||
# Remove the book with the `book name`.
|
||||
dnote remove -b JS
|
||||
|
|
|
|||
|
|
@ -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(¬eRowID)
|
||||
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
85
cli/cmd/add/add_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(¬eUUID, &oldContent)
|
||||
err := db.QueryRow("SELECT uuid, body FROM notes WHERE rowid = ? AND deleted = false", noteRowID).Scan(¬eUUID, &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")
|
||||
|
|
|
|||
|
|
@ -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(¬eUUID, ¬eContent)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -16,15 +16,26 @@
|
|||
* along with Dnote CLI. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package log
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/dnote/dnote/cli/log"
|
||||
)
|
||||
|
||||
// PrintContent prints the note content with an appropriate format.
|
||||
func PrintContent(content string) {
|
||||
fmt.Printf("\n-----------------------content-----------------------\n")
|
||||
fmt.Printf("%s", content)
|
||||
fmt.Printf("\n-----------------------------------------------------\n")
|
||||
// PrintNoteInfo prints a note information
|
||||
func PrintNoteInfo(info NoteInfo) {
|
||||
log.Infof("book name: %s\n", info.BookLabel)
|
||||
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)"))
|
||||
}
|
||||
log.Infof("note id: %d\n", info.RowID)
|
||||
log.Infof("note uuid: %s\n", info.UUID)
|
||||
|
||||
fmt.Printf("\n------------------------content------------------------\n")
|
||||
fmt.Printf("%s", info.Content)
|
||||
fmt.Printf("\n-------------------------------------------------------\n")
|
||||
}
|
||||
38
cli/core/queries.go
Normal file
38
cli/core/queries.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/dnote/dnote/cli/infra"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NoteInfo is a basic information about a note
|
||||
type NoteInfo struct {
|
||||
RowID int
|
||||
BookLabel string
|
||||
UUID string
|
||||
Content string
|
||||
AddedOn int64
|
||||
EditedOn int64
|
||||
}
|
||||
|
||||
// GetNoteInfo returns a NoteInfo for the note with the given noteRowID
|
||||
func GetNoteInfo(ctx infra.DnoteCtx, noteRowID string) (NoteInfo, error) {
|
||||
var ret NoteInfo
|
||||
|
||||
db := ctx.DB
|
||||
err := db.QueryRow(`SELECT books.label, notes.uuid, notes.body, notes.added_on, notes.edited_on, notes.rowid
|
||||
FROM notes
|
||||
INNER JOIN books ON books.uuid = notes.book_uuid
|
||||
WHERE notes.rowid = ? AND notes.deleted = false`, noteRowID).
|
||||
Scan(&ret.BookLabel, &ret.UUID, &ret.Content, &ret.AddedOn, &ret.EditedOn, &ret.RowID)
|
||||
if err == sql.ErrNoRows {
|
||||
return ret, errors.Errorf("note %s not found", noteRowID)
|
||||
} else if err != nil {
|
||||
return ret, errors.Wrap(err, "querying the note")
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
|
||||
}
|
||||
|
|
@ -52,7 +52,7 @@ func TestInit(t *testing.T) {
|
|||
testutils.RunDnoteCmd(t, ctx, binaryName)
|
||||
|
||||
// Test
|
||||
if !utils.FileExists(fmt.Sprintf("%s", ctx.DnoteDir)) {
|
||||
if !utils.FileExists(ctx.DnoteDir) {
|
||||
t.Errorf("dnote directory was not initialized")
|
||||
}
|
||||
if !utils.FileExists(fmt.Sprintf("%s/%s", ctx.DnoteDir, core.ConfigFilename)) {
|
||||
|
|
@ -168,7 +168,7 @@ func TestEditNote_BodyFlag(t *testing.T) {
|
|||
testutils.Setup4(t, ctx)
|
||||
|
||||
// Execute
|
||||
testutils.RunDnoteCmd(t, ctx, binaryName, "edit", "js", "2", "-c", "foo bar")
|
||||
testutils.RunDnoteCmd(t, ctx, binaryName, "edit", "2", "-c", "foo bar")
|
||||
|
||||
// Test
|
||||
db := ctx.DB
|
||||
|
|
|
|||
50
cli/migrate/fixtures/local-10-pre-schema.sql
Normal file
50
cli/migrate/fixtures/local-10-pre-schema.sql
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
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_books_uuid ON books(uuid);
|
||||
CREATE TABLE IF NOT EXISTS "notes"
|
||||
(
|
||||
uuid text NOT NULL,
|
||||
book_uuid text NOT NULL,
|
||||
body 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 VIRTUAL TABLE note_fts USING fts5(content=notes, body, tokenize="porter unicode61 categories 'L* N* Co Ps Pe'")
|
||||
/* note_fts(body) */;
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_data'(id INTEGER PRIMARY KEY, block BLOB);
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_docsize'(id INTEGER PRIMARY KEY, sz BLOB);
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_config'(k PRIMARY KEY, v) WITHOUT ROWID;
|
||||
CREATE TRIGGER notes_after_insert AFTER INSERT ON notes BEGIN
|
||||
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
|
||||
END;
|
||||
CREATE TRIGGER notes_after_delete AFTER DELETE ON notes BEGIN
|
||||
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
|
||||
END;
|
||||
CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
|
||||
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
|
||||
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
|
||||
END;
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
|
||||
|
|
@ -43,6 +43,7 @@ var LocalSequence = []migration{
|
|||
lm7,
|
||||
lm8,
|
||||
lm9,
|
||||
lm10,
|
||||
}
|
||||
|
||||
// RemoteSequence is a list of remote migrations to be run
|
||||
|
|
|
|||
|
|
@ -890,6 +890,77 @@ func TestLocalMigration9(t *testing.T) {
|
|||
testutils.AssertEqual(t, resCount, 1, "noteFtsCount mismatch")
|
||||
}
|
||||
|
||||
func TestLocalMigration10(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/local-10-pre-schema.sql", false)
|
||||
defer testutils.TeardownEnv(ctx)
|
||||
|
||||
db := ctx.DB
|
||||
|
||||
b1UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "123")
|
||||
b2UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "123 javascript")
|
||||
b3UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 3", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b3UUID, "foo")
|
||||
b4UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 4", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b4UUID, "+123")
|
||||
b5UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 5", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b5UUID, "0123")
|
||||
b6UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 6", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b6UUID, "javascript 123")
|
||||
b7UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 7", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b7UUID, "123 (1)")
|
||||
b8UUID := utils.GenerateUUID()
|
||||
testutils.MustExec(t, "inserting book 8", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b8UUID, "5")
|
||||
|
||||
// Execute
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
t.Fatal(errors.Wrap(err, "beginning a transaction"))
|
||||
}
|
||||
|
||||
err = lm10.run(ctx, tx)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
t.Fatal(errors.Wrap(err, "failed to run"))
|
||||
}
|
||||
|
||||
tx.Commit()
|
||||
|
||||
// Test
|
||||
|
||||
// assert that note_fts was populated with correct values
|
||||
var b1Label, b2Label, b3Label, b4Label, b5Label, b6Label, b7Label, b8Label string
|
||||
var b1Dirty, b2Dirty, b3Dirty, b4Dirty, b5Dirty, b6Dirty, b7Dirty, b8Dirty bool
|
||||
|
||||
testutils.MustScan(t, "getting b1", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b1UUID), &b1Label, &b1Dirty)
|
||||
testutils.MustScan(t, "getting b2", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b2UUID), &b2Label, &b2Dirty)
|
||||
testutils.MustScan(t, "getting b3", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b3UUID), &b3Label, &b3Dirty)
|
||||
testutils.MustScan(t, "getting b4", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b4UUID), &b4Label, &b4Dirty)
|
||||
testutils.MustScan(t, "getting b5", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b5UUID), &b5Label, &b5Dirty)
|
||||
testutils.MustScan(t, "getting b6", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b6UUID), &b6Label, &b6Dirty)
|
||||
testutils.MustScan(t, "getting b7", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b7UUID), &b7Label, &b7Dirty)
|
||||
testutils.MustScan(t, "getting b8", db.QueryRow("SELECT label, dirty FROM books WHERE uuid = ?", b8UUID), &b8Label, &b8Dirty)
|
||||
|
||||
testutils.AssertEqual(t, b1Label, "123 (2)", "b1Label mismatch")
|
||||
testutils.AssertEqual(t, b1Dirty, true, "b1Dirty mismatch")
|
||||
testutils.AssertEqual(t, b2Label, "123 javascript", "b2Label mismatch")
|
||||
testutils.AssertEqual(t, b2Dirty, false, "b2Dirty mismatch")
|
||||
testutils.AssertEqual(t, b3Label, "foo", "b3Label mismatch")
|
||||
testutils.AssertEqual(t, b3Dirty, false, "b3Dirty mismatch")
|
||||
testutils.AssertEqual(t, b4Label, "+123", "b4Label mismatch")
|
||||
testutils.AssertEqual(t, b4Dirty, false, "b4Dirty mismatch")
|
||||
testutils.AssertEqual(t, b5Label, "0123 (1)", "b5Label mismatch")
|
||||
testutils.AssertEqual(t, b5Dirty, true, "b5Dirty mismatch")
|
||||
testutils.AssertEqual(t, b6Label, "javascript 123", "b6Label mismatch")
|
||||
testutils.AssertEqual(t, b6Dirty, false, "b6Dirty mismatch")
|
||||
testutils.AssertEqual(t, b7Label, "123 (1)", "b7Label mismatch")
|
||||
testutils.AssertEqual(t, b7Dirty, false, "b7Dirty mismatch")
|
||||
testutils.AssertEqual(t, b8Label, "5 (1)", "b8Label mismatch")
|
||||
testutils.AssertEqual(t, b8Dirty, true, "b8Dirty mismatch")
|
||||
}
|
||||
|
||||
func TestRemoteMigration1(t *testing.T) {
|
||||
// set up
|
||||
ctx := testutils.InitEnv(t, "../tmp", "./fixtures/remote-1-pre-schema.sql", false)
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import (
|
|||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
|
||||
"github.com/dnote/actions"
|
||||
"github.com/dnote/dnote/cli/client"
|
||||
|
|
@ -403,6 +404,66 @@ var lm9 = migration{
|
|||
},
|
||||
}
|
||||
|
||||
var lm10 = migration{
|
||||
name: "rename-number-only-book",
|
||||
run: func(ctx infra.DnoteCtx, tx *infra.DB) error {
|
||||
migrateBook := func(label string) error {
|
||||
var uuid string
|
||||
|
||||
err := tx.QueryRow("SELECT uuid FROM books WHERE label = ?", label).Scan(&uuid)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "finding uuid")
|
||||
}
|
||||
|
||||
for i := 1; ; i++ {
|
||||
candidate := fmt.Sprintf("%s (%d)", label, 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'", label)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
rows, err := tx.Query("SELECT label FROM books")
|
||||
defer rows.Close()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "getting labels")
|
||||
}
|
||||
|
||||
var regexNumber = regexp.MustCompile(`^\d+$`)
|
||||
|
||||
for rows.Next() {
|
||||
var label string
|
||||
err := rows.Scan(&label)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "scannign row")
|
||||
}
|
||||
|
||||
if regexNumber.MatchString(label) {
|
||||
err = migrateBook(label)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "migrating book %s", label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var rm1 = migration{
|
||||
name: "sync-book-uuids-from-server",
|
||||
run: func(ctx infra.DnoteCtx, tx *infra.DB) error {
|
||||
|
|
|
|||
|
|
@ -10,14 +10,6 @@ CREATE TABLE system
|
|||
);
|
||||
CREATE UNIQUE INDEX idx_books_label ON books(label);
|
||||
CREATE UNIQUE INDEX idx_books_uuid ON books(uuid);
|
||||
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 IF NOT EXISTS "notes"
|
||||
(
|
||||
uuid text NOT NULL,
|
||||
|
|
@ -30,8 +22,7 @@ CREATE TABLE IF NOT EXISTS "notes"
|
|||
usn int DEFAULT 0 NOT NULL,
|
||||
deleted bool DEFAULT false
|
||||
);
|
||||
CREATE VIRTUAL TABLE note_fts
|
||||
USING fts5(content=notes, body, tokenize = "unicode61")
|
||||
CREATE VIRTUAL TABLE note_fts USING fts5(content=notes, body, tokenize="porter unicode61 categories 'L* N* Co Ps Pe'")
|
||||
/* note_fts(body) */;
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_data'(id INTEGER PRIMARY KEY, block BLOB);
|
||||
CREATE TABLE IF NOT EXISTS 'note_fts_idx'(segid, term, pgno, PRIMARY KEY(segid, term)) WITHOUT ROWID;
|
||||
|
|
@ -47,5 +38,13 @@ CREATE TRIGGER notes_after_update AFTER UPDATE ON notes BEGIN
|
|||
INSERT INTO note_fts(note_fts, rowid, body) VALUES ('delete', old.rowid, old.body);
|
||||
INSERT INTO note_fts(rowid, body) VALUES (new.rowid, new.body);
|
||||
END;
|
||||
CREATE TABLE actions
|
||||
(
|
||||
uuid text PRIMARY KEY,
|
||||
schema integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL,
|
||||
timestamp integer NOT NULL
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_notes_uuid ON notes(uuid);
|
||||
CREATE INDEX idx_notes_book_uuid ON notes(book_uuid);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
|
|
@ -248,3 +249,15 @@ func CopyFile(src, dest string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
// regexNumber is a regex that matches a string that looks like an integer
|
||||
var regexNumber = regexp.MustCompile(`^\d+$`)
|
||||
|
||||
// IsNumber checks if the given string is in the form of a number
|
||||
func IsNumber(s string) bool {
|
||||
if s == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
return regexNumber.MatchString(s)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue