diff --git a/go.mod b/go.mod index 48dfc85c..3b87c5ec 100644 --- a/go.mod +++ b/go.mod @@ -14,9 +14,10 @@ require ( github.com/mattn/go-sqlite3 v1.14.32 github.com/pkg/errors v0.9.1 github.com/radovskyb/watcher v1.0.7 + github.com/robfig/cron v1.2.0 github.com/sergi/go-diff v1.3.1 github.com/spf13/cobra v1.10.1 - golang.org/x/crypto v0.45.0 + golang.org/x/crypto v0.42.0 golang.org/x/time v0.13.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/yaml.v2 v2.4.0 @@ -35,9 +36,9 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/testify v1.8.1 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/term v0.37.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/term v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 8b3c653a..e175da0d 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE= github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -71,15 +73,15 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= +golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/cli/cmd/add/add.go b/pkg/cli/cmd/add/add.go index 3e6d089d..be89b829 100644 --- a/pkg/cli/cmd/add/add.go +++ b/pkg/cli/cmd/add/add.go @@ -131,7 +131,7 @@ func newRun(ctx context.DnoteCtx) infra.RunEFunc { return err } - output.NoteInfo(os.Stdout, info) + output.NoteInfo(info) if err := upgrade.Check(ctx); err != nil { log.Error(errors.Wrap(err, "automatically checking updates").Error()) diff --git a/pkg/cli/cmd/cat/cat.go b/pkg/cli/cmd/cat/cat.go new file mode 100644 index 00000000..c32eb687 --- /dev/null +++ b/pkg/cli/cmd/cat/cat.go @@ -0,0 +1,95 @@ +/* Copyright 2025 Dnote Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cat + +import ( + "strconv" + + "github.com/dnote/dnote/pkg/cli/context" + "github.com/dnote/dnote/pkg/cli/database" + "github.com/dnote/dnote/pkg/cli/infra" + "github.com/dnote/dnote/pkg/cli/log" + "github.com/dnote/dnote/pkg/cli/output" + "github.com/pkg/errors" + "github.com/spf13/cobra" +) + +var example = ` + * See the notes with index 2 from a book 'javascript' + dnote cat javascript 2 + ` + +var deprecationWarning = `and "view" will replace it in the future version. + + Run "dnote view --help" for more information. +` + +func preRun(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return errors.New("Incorrect number of arguments") + } + + return nil +} + +// NewCmd returns a new cat command +func NewCmd(ctx context.DnoteCtx) *cobra.Command { + cmd := &cobra.Command{ + Use: "cat ", + Aliases: []string{"c"}, + Short: "See a note", + Example: example, + RunE: NewRun(ctx, false), + PreRunE: preRun, + Deprecated: deprecationWarning, + } + + return cmd +} + +// NewRun returns a new run function +func NewRun(ctx context.DnoteCtx, contentOnly bool) infra.RunEFunc { + return func(cmd *cobra.Command, args []string) error { + var noteRowIDArg 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")) + + noteRowIDArg = args[1] + } else { + noteRowIDArg = args[0] + } + + noteRowID, err := strconv.Atoi(noteRowIDArg) + if err != nil { + return errors.Wrap(err, "invalid rowid") + } + + db := ctx.DB + info, err := database.GetNoteInfo(db, noteRowID) + if err != nil { + return err + } + + if contentOnly { + output.NoteContent(info) + } else { + output.NoteInfo(info) + } + + return nil + } +} diff --git a/pkg/cli/cmd/edit/note.go b/pkg/cli/cmd/edit/note.go index cb837e11..84a631f4 100644 --- a/pkg/cli/cmd/edit/note.go +++ b/pkg/cli/cmd/edit/note.go @@ -166,7 +166,7 @@ func runNote(ctx context.DnoteCtx, rowIDArg string) error { } log.Success("edited the note\n") - output.NoteInfo(os.Stdout, noteInfo) + output.NoteInfo(noteInfo) return nil } diff --git a/pkg/cli/cmd/view/book.go b/pkg/cli/cmd/ls/ls.go similarity index 63% rename from pkg/cli/cmd/view/book.go rename to pkg/cli/cmd/ls/ls.go index 698a7de5..f0ddd047 100644 --- a/pkg/cli/cmd/view/book.go +++ b/pkg/cli/cmd/ls/ls.go @@ -13,19 +13,76 @@ * limitations under the License. */ -package view +package ls import ( "database/sql" "fmt" - "io" "strings" "github.com/dnote/dnote/pkg/cli/context" + "github.com/dnote/dnote/pkg/cli/infra" "github.com/dnote/dnote/pkg/cli/log" "github.com/pkg/errors" + "github.com/spf13/cobra" ) +var example = ` + * List all books + dnote ls + + * List notes in a book + dnote ls javascript + ` + +var deprecationWarning = `and "view" will replace it in the future version. + +Run "dnote view --help" for more information. +` + +func preRun(cmd *cobra.Command, args []string) error { + if len(args) > 1 { + return errors.New("Incorrect number of argument") + } + + return nil +} + +// NewCmd returns a new ls command +func NewCmd(ctx context.DnoteCtx) *cobra.Command { + cmd := &cobra.Command{ + Use: "ls ", + Aliases: []string{"l", "notes"}, + Short: "List all notes", + Example: example, + RunE: NewRun(ctx, false), + PreRunE: preRun, + Deprecated: deprecationWarning, + } + + return cmd +} + +// NewRun returns a new run function for ls +func NewRun(ctx context.DnoteCtx, nameOnly bool) infra.RunEFunc { + return func(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + if err := printBooks(ctx, nameOnly); err != nil { + return errors.Wrap(err, "viewing books") + } + + return nil + } + + bookName := args[0] + if err := printNotes(ctx, bookName); err != nil { + return errors.Wrapf(err, "viewing book '%s'", bookName) + } + + return nil + } +} + // bookInfo is an information about the book to be printed on screen type bookInfo struct { BookLabel string @@ -40,13 +97,15 @@ type noteInfo struct { // getNewlineIdx returns the index of newline character in a string func getNewlineIdx(str string) int { - // Check for \r\n first - if idx := strings.Index(str, "\r\n"); idx != -1 { - return idx + var ret int + + ret = strings.Index(str, "\n") + + if ret == -1 { + ret = strings.Index(str, "\r\n") } - // Then check for \n - return strings.Index(str, "\n") + return ret } // formatBody returns an excerpt of the given raw note content and a boolean @@ -64,15 +123,15 @@ func formatBody(noteBody string) (string, bool) { return strings.Trim(trimmed, " "), false } -func printBookLine(w io.Writer, info bookInfo, nameOnly bool) { +func printBookLine(info bookInfo, nameOnly bool) { if nameOnly { - fmt.Fprintln(w, info.BookLabel) + fmt.Println(info.BookLabel) } else { - fmt.Fprintf(w, "%s %s\n", info.BookLabel, log.ColorYellow.Sprintf("(%d)", info.NoteCount)) + log.Printf("%s %s\n", info.BookLabel, log.ColorYellow.Sprintf("(%d)", info.NoteCount)) } } -func listBooks(ctx context.DnoteCtx, w io.Writer, nameOnly bool) error { +func printBooks(ctx context.DnoteCtx, nameOnly bool) error { db := ctx.DB rows, err := db.Query(`SELECT books.label, count(notes.uuid) note_count @@ -98,13 +157,13 @@ func listBooks(ctx context.DnoteCtx, w io.Writer, nameOnly bool) error { } for _, info := range infos { - printBookLine(w, info, nameOnly) + printBookLine(info, nameOnly) } return nil } -func listNotes(ctx context.DnoteCtx, w io.Writer, bookName string) error { +func printNotes(ctx context.DnoteCtx, bookName string) error { db := ctx.DB var bookUUID string @@ -132,7 +191,7 @@ func listNotes(ctx context.DnoteCtx, w io.Writer, bookName string) error { infos = append(infos, info) } - fmt.Fprintf(w, "on book %s\n", bookName) + log.Infof("on book %s\n", bookName) for _, info := range infos { body, isExcerpt := formatBody(info.Body) @@ -142,7 +201,7 @@ func listNotes(ctx context.DnoteCtx, w io.Writer, bookName string) error { body = fmt.Sprintf("%s %s", body, log.ColorYellow.Sprintf("[---More---]")) } - fmt.Fprintf(w, "%s %s\n", rowid, body) + log.Plainf("%s %s\n", rowid, body) } return nil diff --git a/pkg/cli/cmd/remove/remove.go b/pkg/cli/cmd/remove/remove.go index 18224c0f..89d44b78 100644 --- a/pkg/cli/cmd/remove/remove.go +++ b/pkg/cli/cmd/remove/remove.go @@ -17,7 +17,6 @@ package remove import ( "fmt" - "os" "strconv" "github.com/dnote/dnote/pkg/cli/context" @@ -130,7 +129,7 @@ func runNote(ctx context.DnoteCtx, rowIDArg string) error { return err } - output.NoteInfo(os.Stdout, noteInfo) + output.NoteInfo(noteInfo) ok, err := maybeConfirm("remove this note?", false) if err != nil { diff --git a/pkg/cli/cmd/view/book_test.go b/pkg/cli/cmd/view/book_test.go deleted file mode 100644 index 226d5d04..00000000 --- a/pkg/cli/cmd/view/book_test.go +++ /dev/null @@ -1,184 +0,0 @@ -/* Copyright 2025 Dnote Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package view - -import ( - "bytes" - "fmt" - "strings" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/cli/context" - "github.com/dnote/dnote/pkg/cli/database" -) - -func TestGetNewlineIdx(t *testing.T) { - testCases := []struct { - input string - expected int - }{ - { - input: "hello\nworld", - expected: 5, - }, - { - input: "hello\r\nworld", - expected: 5, - }, - { - input: "no newline here", - expected: -1, - }, - { - input: "", - expected: -1, - }, - { - input: "\n", - expected: 0, - }, - { - input: "\r\n", - expected: 0, - }, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("input: %q", tc.input), func(t *testing.T) { - got := getNewlineIdx(tc.input) - assert.Equal(t, got, tc.expected, "newline index mismatch") - }) - } -} - -func TestFormatBody(t *testing.T) { - testCases := []struct { - input string - expectedBody string - expectedExcerpt bool - }{ - { - input: "single line", - expectedBody: "single line", - expectedExcerpt: false, - }, - { - input: "first line\nsecond line", - expectedBody: "first line", - expectedExcerpt: true, - }, - { - input: "first line\r\nsecond line", - expectedBody: "first line", - expectedExcerpt: true, - }, - { - input: " spaced line ", - expectedBody: "spaced line", - expectedExcerpt: false, - }, - { - input: " first line \nsecond line", - expectedBody: "first line", - expectedExcerpt: true, - }, - { - input: "", - expectedBody: "", - expectedExcerpt: false, - }, - { - input: "line with trailing newline\n", - expectedBody: "line with trailing newline", - expectedExcerpt: false, - }, - { - input: "line with trailing newlines\n\n", - expectedBody: "line with trailing newlines", - expectedExcerpt: false, - }, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("input: %q", tc.input), func(t *testing.T) { - gotBody, gotExcerpt := formatBody(tc.input) - assert.Equal(t, gotBody, tc.expectedBody, "formatted body mismatch") - assert.Equal(t, gotExcerpt, tc.expectedExcerpt, "excerpt flag mismatch") - }) - } -} - -func TestListNotes(t *testing.T) { - // Setup - db := database.InitTestMemoryDB(t) - defer db.Close() - - bookUUID := "js-book-uuid" - database.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, "javascript") - database.MustExec(t, "inserting note 1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-1", bookUUID, "first note", 1515199943) - database.MustExec(t, "inserting note 2", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-2", bookUUID, "multiline note\nwith second line", 1515199945) - - ctx := context.DnoteCtx{DB: db} - var buf bytes.Buffer - - // Execute - err := listNotes(ctx, &buf, "javascript") - if err != nil { - t.Fatal(err) - } - - got := buf.String() - - // Verify output - assert.Equal(t, strings.Contains(got, "on book javascript"), true, "should show book name") - assert.Equal(t, strings.Contains(got, "first note"), true, "should contain first note") - assert.Equal(t, strings.Contains(got, "multiline note"), true, "should show first line of multiline note") - assert.Equal(t, strings.Contains(got, "[---More---]"), true, "should show more indicator for multiline note") - assert.Equal(t, strings.Contains(got, "with second line"), false, "should not show second line of multiline note") -} - -func TestListBooks(t *testing.T) { - // Setup - db := database.InitTestMemoryDB(t) - defer db.Close() - - b1UUID := "js-book-uuid" - b2UUID := "linux-book-uuid" - - database.MustExec(t, "inserting book 1", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b1UUID, "javascript") - database.MustExec(t, "inserting book 2", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", b2UUID, "linux") - - // Add notes to test count - database.MustExec(t, "inserting note 1", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-1", b1UUID, "note body 1", 1515199943) - database.MustExec(t, "inserting note 2", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", "note-2", b1UUID, "note body 2", 1515199944) - - ctx := context.DnoteCtx{DB: db} - var buf bytes.Buffer - - // Execute - err := listBooks(ctx, &buf, false) - if err != nil { - t.Fatal(err) - } - - got := buf.String() - - // Verify output - assert.Equal(t, strings.Contains(got, "javascript"), true, "should contain javascript book") - assert.Equal(t, strings.Contains(got, "linux"), true, "should contain linux book") - assert.Equal(t, strings.Contains(got, "(2)"), true, "should show 2 notes for javascript") -} diff --git a/pkg/cli/cmd/view/note.go b/pkg/cli/cmd/view/note.go deleted file mode 100644 index f853dd9a..00000000 --- a/pkg/cli/cmd/view/note.go +++ /dev/null @@ -1,47 +0,0 @@ -/* Copyright 2025 Dnote Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package view - -import ( - "io" - "strconv" - - "github.com/dnote/dnote/pkg/cli/context" - "github.com/dnote/dnote/pkg/cli/database" - "github.com/dnote/dnote/pkg/cli/output" - "github.com/pkg/errors" -) - -func viewNote(ctx context.DnoteCtx, w io.Writer, noteRowIDArg string, contentOnly bool) error { - noteRowID, err := strconv.Atoi(noteRowIDArg) - if err != nil { - return errors.Wrap(err, "invalid rowid") - } - - db := ctx.DB - info, err := database.GetNoteInfo(db, noteRowID) - if err != nil { - return err - } - - if contentOnly { - output.NoteContent(w, info) - } else { - output.NoteInfo(w, info) - } - - return nil -} diff --git a/pkg/cli/cmd/view/note_test.go b/pkg/cli/cmd/view/note_test.go deleted file mode 100644 index 36e9aa84..00000000 --- a/pkg/cli/cmd/view/note_test.go +++ /dev/null @@ -1,90 +0,0 @@ -/* Copyright 2025 Dnote Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package view - -import ( - "bytes" - "strings" - "testing" - - "github.com/dnote/dnote/pkg/assert" - "github.com/dnote/dnote/pkg/cli/context" - "github.com/dnote/dnote/pkg/cli/database" -) - -func TestViewNote(t *testing.T) { - db := database.InitTestMemoryDB(t) - defer db.Close() - - bookUUID := "test-book-uuid" - database.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, "golang") - database.MustExec(t, "inserting note", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", - "note-uuid", bookUUID, "test note content", 1515199943000000000) - - ctx := context.DnoteCtx{DB: db} - var buf bytes.Buffer - - err := viewNote(ctx, &buf, "1", false) - if err != nil { - t.Fatal(err) - } - - got := buf.String() - assert.Equal(t, strings.Contains(got, "test note content"), true, "should contain note content") -} - -func TestViewNoteContentOnly(t *testing.T) { - db := database.InitTestMemoryDB(t) - defer db.Close() - - bookUUID := "test-book-uuid" - database.MustExec(t, "inserting book", db, "INSERT INTO books (uuid, label) VALUES (?, ?)", bookUUID, "golang") - database.MustExec(t, "inserting note", db, "INSERT INTO notes (uuid, book_uuid, body, added_on) VALUES (?, ?, ?, ?)", - "note-uuid", bookUUID, "test note content", 1515199943000000000) - - ctx := context.DnoteCtx{DB: db} - var buf bytes.Buffer - - err := viewNote(ctx, &buf, "1", true) - if err != nil { - t.Fatal(err) - } - - got := buf.String() - assert.Equal(t, got, "test note content", "should contain only note content") -} - -func TestViewNoteInvalidRowID(t *testing.T) { - db := database.InitTestMemoryDB(t) - defer db.Close() - - ctx := context.DnoteCtx{DB: db} - var buf bytes.Buffer - - err := viewNote(ctx, &buf, "not-a-number", false) - assert.NotEqual(t, err, nil, "should return error for invalid rowid") -} - -func TestViewNoteNotFound(t *testing.T) { - db := database.InitTestMemoryDB(t) - defer db.Close() - - ctx := context.DnoteCtx{DB: db} - var buf bytes.Buffer - - err := viewNote(ctx, &buf, "999", false) - assert.NotEqual(t, err, nil, "should return error for non-existent note") -} diff --git a/pkg/cli/cmd/view/view.go b/pkg/cli/cmd/view/view.go index 57b17dbd..62f50a53 100644 --- a/pkg/cli/cmd/view/view.go +++ b/pkg/cli/cmd/view/view.go @@ -16,13 +16,14 @@ package view import ( - "os" - "github.com/dnote/dnote/pkg/cli/context" "github.com/dnote/dnote/pkg/cli/infra" - "github.com/dnote/dnote/pkg/cli/utils" "github.com/pkg/errors" "github.com/spf13/cobra" + + "github.com/dnote/dnote/pkg/cli/cmd/cat" + "github.com/dnote/dnote/pkg/cli/cmd/ls" + "github.com/dnote/dnote/pkg/cli/utils" ) var example = ` @@ -67,26 +68,27 @@ func NewCmd(ctx context.DnoteCtx) *cobra.Command { func newRun(ctx context.DnoteCtx) infra.RunEFunc { return func(cmd *cobra.Command, args []string) error { + var run infra.RunEFunc + if len(args) == 0 { - // List all books - return listBooks(ctx, os.Stdout, nameOnly) + run = ls.NewRun(ctx, nameOnly) } else if len(args) == 1 { if nameOnly { return errors.New("--name-only flag is only valid when viewing books") } if utils.IsNumber(args[0]) { - // View a note by index - return viewNote(ctx, os.Stdout, args[0], contentOnly) + run = cat.NewRun(ctx, contentOnly) } else { - // List notes in a book - return listNotes(ctx, os.Stdout, args[0]) + run = ls.NewRun(ctx, false) } } else if len(args) == 2 { - // View a note in a book (book name + note index) - return viewNote(ctx, os.Stdout, args[1], contentOnly) + // DEPRECATED: passing book name to view command is deprecated + run = cat.NewRun(ctx, false) + } else { + return errors.New("Incorrect number of arguments") } - return errors.New("Incorrect number of arguments") + return run(cmd, args) } } diff --git a/pkg/cli/main.go b/pkg/cli/main.go index 2fb1c564..2afbaf36 100644 --- a/pkg/cli/main.go +++ b/pkg/cli/main.go @@ -26,10 +26,12 @@ import ( // commands "github.com/dnote/dnote/pkg/cli/cmd/add" + "github.com/dnote/dnote/pkg/cli/cmd/cat" "github.com/dnote/dnote/pkg/cli/cmd/edit" "github.com/dnote/dnote/pkg/cli/cmd/find" "github.com/dnote/dnote/pkg/cli/cmd/login" "github.com/dnote/dnote/pkg/cli/cmd/logout" + "github.com/dnote/dnote/pkg/cli/cmd/ls" "github.com/dnote/dnote/pkg/cli/cmd/remove" "github.com/dnote/dnote/pkg/cli/cmd/root" "github.com/dnote/dnote/pkg/cli/cmd/sync" @@ -77,8 +79,10 @@ func main() { root.Register(login.NewCmd(*ctx)) root.Register(logout.NewCmd(*ctx)) root.Register(add.NewCmd(*ctx)) + root.Register(ls.NewCmd(*ctx)) root.Register(sync.NewCmd(*ctx)) root.Register(version.NewCmd(*ctx)) + root.Register(cat.NewCmd(*ctx)) root.Register(view.NewCmd(*ctx)) root.Register(find.NewCmd(*ctx)) diff --git a/pkg/cli/main_test.go b/pkg/cli/main_test.go index 727a5c3e..5da1915e 100644 --- a/pkg/cli/main_test.go +++ b/pkg/cli/main_test.go @@ -20,7 +20,6 @@ import ( "log" "os" "os/exec" - "strings" "testing" "github.com/dnote/dnote/pkg/assert" @@ -569,65 +568,3 @@ func TestDBPathFlag(t *testing.T) { db2.QueryRow("SELECT count(*) FROM books WHERE label = ?", "db1-book").Scan(&db2HasDB1Book) assert.Equal(t, db2HasDB1Book, 0, "db2 should not have db1's book") } - -func TestView(t *testing.T) { - t.Run("view note by rowid", func(t *testing.T) { - _, opts := setupTestEnv(t) - - db, dbPath := database.InitTestFileDB(t) - testutils.Setup4(t, db) - - output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "1") - - assert.Equal(t, strings.Contains(output, "Booleans have toString()"), true, "should contain note content") - assert.Equal(t, strings.Contains(output, "book name"), true, "should show metadata") - }) - - t.Run("view note content only", func(t *testing.T) { - _, opts := setupTestEnv(t) - - db, dbPath := database.InitTestFileDB(t) - testutils.Setup4(t, db) - - output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "1", "--content-only") - - assert.Equal(t, strings.Contains(output, "Booleans have toString()"), true, "should contain note content") - assert.Equal(t, strings.Contains(output, "book name"), false, "should not show metadata") - }) - - t.Run("list books", func(t *testing.T) { - _, opts := setupTestEnv(t) - - db, dbPath := database.InitTestFileDB(t) - testutils.Setup1(t, db) - - output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view") - - assert.Equal(t, strings.Contains(output, "js"), true, "should list js book") - assert.Equal(t, strings.Contains(output, "linux"), true, "should list linux book") - }) - - t.Run("list notes in book", func(t *testing.T) { - _, opts := setupTestEnv(t) - - db, dbPath := database.InitTestFileDB(t) - testutils.Setup2(t, db) - - output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "js") - - assert.Equal(t, strings.Contains(output, "n1 body"), true, "should list note 1") - assert.Equal(t, strings.Contains(output, "n2 body"), true, "should list note 2") - }) - - t.Run("view note by book name and rowid", func(t *testing.T) { - _, opts := setupTestEnv(t) - - db, dbPath := database.InitTestFileDB(t) - testutils.Setup4(t, db) - - output := testutils.RunDnoteCmd(t, opts, binaryName, "--dbPath", dbPath, "view", "js", "2") - - assert.Equal(t, strings.Contains(output, "Date object implements mathematical comparisons"), true, "should contain note content") - assert.Equal(t, strings.Contains(output, "book name"), true, "should show metadata") - }) -} diff --git a/pkg/cli/output/output.go b/pkg/cli/output/output.go index d272ba88..fe6e3c87 100644 --- a/pkg/cli/output/output.go +++ b/pkg/cli/output/output.go @@ -19,7 +19,6 @@ package output import ( "fmt" - "io" "time" "github.com/dnote/dnote/pkg/cli/database" @@ -27,7 +26,7 @@ import ( ) // NoteInfo prints a note information -func NoteInfo(w io.Writer, info database.NoteInfo) { +func NoteInfo(info database.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 { @@ -36,13 +35,13 @@ func NoteInfo(w io.Writer, info database.NoteInfo) { log.Infof("note id: %d\n", info.RowID) log.Infof("note uuid: %s\n", info.UUID) - fmt.Fprintf(w, "\n------------------------content------------------------\n") - fmt.Fprintf(w, "%s", info.Content) - fmt.Fprintf(w, "\n-------------------------------------------------------\n") + fmt.Printf("\n------------------------content------------------------\n") + fmt.Printf("%s", info.Content) + fmt.Printf("\n-------------------------------------------------------\n") } -func NoteContent(w io.Writer, info database.NoteInfo) { - fmt.Fprintf(w, "%s", info.Content) +func NoteContent(info database.NoteInfo) { + fmt.Printf("%s", info.Content) } // BookInfo prints a note information diff --git a/pkg/cli/testutils/main.go b/pkg/cli/testutils/main.go index db3282d7..581cbd7f 100644 --- a/pkg/cli/testutils/main.go +++ b/pkg/cli/testutils/main.go @@ -144,7 +144,7 @@ type RunDnoteCmdOptions struct { } // RunDnoteCmd runs a dnote command -func RunDnoteCmd(t *testing.T, opts RunDnoteCmdOptions, binaryName string, arg ...string) string { +func RunDnoteCmd(t *testing.T, opts RunDnoteCmdOptions, binaryName string, arg ...string) { t.Logf("running: %s %s", binaryName, strings.Join(arg, " ")) cmd, stderr, stdout, err := NewDnoteCmd(opts, binaryName, arg...) @@ -162,8 +162,6 @@ func RunDnoteCmd(t *testing.T, opts RunDnoteCmdOptions, binaryName string, arg . // Print stdout if and only if test fails later t.Logf("\n%s", stdout) - - return stdout.String() } // WaitDnoteCmd runs a dnote command and passes stdout to the callback. diff --git a/pkg/e2e/server_test.go b/pkg/e2e/server_test.go index e8ca3da6..7c646e8d 100644 --- a/pkg/e2e/server_test.go +++ b/pkg/e2e/server_test.go @@ -331,23 +331,3 @@ func TestServerUserCreateHelp(t *testing.T) { assert.Equal(t, strings.Contains(outputStr, "--password"), true, "help should show --password (double dash)") assert.Equal(t, strings.Contains(outputStr, "--dbPath"), true, "help should show --dbPath (double dash)") } - -func TestServerUserList(t *testing.T) { - tmpDB := t.TempDir() + "/test.db" - - // Create two users - exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "alice@example.com", "--password", "password123").CombinedOutput() - exec.Command(testServerBinary, "user", "create", "--dbPath", tmpDB, "--email", "bob@example.com", "--password", "password123").CombinedOutput() - - // List users - listCmd := exec.Command(testServerBinary, "user", "list", "--dbPath", tmpDB) - output, err := listCmd.CombinedOutput() - - if err != nil { - t.Fatalf("user list failed: %v\nOutput: %s", err, output) - } - - outputStr := string(output) - assert.Equal(t, strings.Contains(outputStr, "alice@example.com"), true, "output should have alice") - assert.Equal(t, strings.Contains(outputStr, "bob@example.com"), true, "output should have bob") -} diff --git a/pkg/server/app/users.go b/pkg/server/app/users.go index 9c167b69..760b3354 100644 --- a/pkg/server/app/users.go +++ b/pkg/server/app/users.go @@ -116,17 +116,6 @@ func (a *App) GetUserByEmail(email string) (*database.User, error) { return &user, nil } -// GetAllUsers retrieves all users from the database -func (a *App) GetAllUsers() ([]database.User, error) { - var users []database.User - err := a.DB.Find(&users).Error - if err != nil { - return nil, pkgErrors.Wrap(err, "finding users") - } - - return users, nil -} - // Authenticate authenticates a user func (a *App) Authenticate(email, password string) (*database.User, error) { user, err := a.GetUserByEmail(email) diff --git a/pkg/server/app/users_test.go b/pkg/server/app/users_test.go index 52183520..f345b217 100644 --- a/pkg/server/app/users_test.go +++ b/pkg/server/app/users_test.go @@ -108,72 +108,6 @@ func TestGetUserByEmail(t *testing.T) { }) } -func TestGetAllUsers(t *testing.T) { - t.Run("success with multiple users", func(t *testing.T) { - db := testutils.InitMemoryDB(t) - - user1 := testutils.SetupUserData(db, "alice@example.com", "password123") - user2 := testutils.SetupUserData(db, "bob@example.com", "password123") - user3 := testutils.SetupUserData(db, "charlie@example.com", "password123") - - a := NewTest() - a.DB = db - - users, err := a.GetAllUsers() - - assert.Equal(t, err, nil, "should not error") - assert.Equal(t, len(users), 3, "should return 3 users") - - // Verify all users are returned - emails := make(map[string]bool) - for _, user := range users { - emails[user.Email.String] = true - } - assert.Equal(t, emails["alice@example.com"], true, "alice should be in results") - assert.Equal(t, emails["bob@example.com"], true, "bob should be in results") - assert.Equal(t, emails["charlie@example.com"], true, "charlie should be in results") - - // Verify user details match - for _, user := range users { - if user.Email.String == "alice@example.com" { - assert.Equal(t, user.ID, user1.ID, "alice ID mismatch") - } else if user.Email.String == "bob@example.com" { - assert.Equal(t, user.ID, user2.ID, "bob ID mismatch") - } else if user.Email.String == "charlie@example.com" { - assert.Equal(t, user.ID, user3.ID, "charlie ID mismatch") - } - } - }) - - t.Run("empty database", func(t *testing.T) { - db := testutils.InitMemoryDB(t) - - a := NewTest() - a.DB = db - - users, err := a.GetAllUsers() - - assert.Equal(t, err, nil, "should not error") - assert.Equal(t, len(users), 0, "should return 0 users") - }) - - t.Run("single user", func(t *testing.T) { - db := testutils.InitMemoryDB(t) - - user := testutils.SetupUserData(db, "alice@example.com", "password123") - - a := NewTest() - a.DB = db - - users, err := a.GetAllUsers() - - assert.Equal(t, err, nil, "should not error") - assert.Equal(t, len(users), 1, "should return 1 user") - assert.Equal(t, users[0].Email.String, "alice@example.com", "email mismatch") - assert.Equal(t, users[0].ID, user.ID, "user ID mismatch") - }) -} - func TestCreateUser(t *testing.T) { t.Run("success", func(t *testing.T) { db := testutils.InitMemoryDB(t) diff --git a/pkg/server/assets/package-lock.json b/pkg/server/assets/package-lock.json index 7eb1c411..610d8eb4 100644 --- a/pkg/server/assets/package-lock.json +++ b/pkg/server/assets/package-lock.json @@ -363,11 +363,10 @@ } }, "node_modules/immutable": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", - "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", - "dev": true, - "license": "MIT" + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "dev": true }, "node_modules/is-extglob": { "version": "2.1.1", diff --git a/pkg/server/cmd/helpers.go b/pkg/server/cmd/helpers.go index ba90a7ce..2a322476 100644 --- a/pkg/server/cmd/helpers.go +++ b/pkg/server/cmd/helpers.go @@ -111,8 +111,8 @@ func requireString(fs *flag.FlagSet, value, fieldName string) { } } -// createApp creates config, initializes app, and returns cleanup function -func createApp(fs *flag.FlagSet, dbPath string) (*app.App, func()) { +// setupAppWithDB creates config, initializes app, and returns cleanup function +func setupAppWithDB(fs *flag.FlagSet, dbPath string) (*app.App, func()) { cfg, err := config.New(config.Params{ DBPath: dbPath, }) diff --git a/pkg/server/cmd/user.go b/pkg/server/cmd/user.go index 7a98344d..01b753b1 100644 --- a/pkg/server/cmd/user.go +++ b/pkg/server/cmd/user.go @@ -51,7 +51,7 @@ func userCreateCmd(args []string) { requireString(fs, *email, "email") requireString(fs, *password, "password") - a, cleanup := createApp(fs, *dbPath) + a, cleanup := setupAppWithDB(fs, *dbPath) defer cleanup() _, err := a.CreateUser(*email, *password, *password) @@ -74,7 +74,7 @@ func userRemoveCmd(args []string, stdin io.Reader) { requireString(fs, *email, "email") - a, cleanup := createApp(fs, *dbPath) + a, cleanup := setupAppWithDB(fs, *dbPath) defer cleanup() // Check if user exists first @@ -127,7 +127,7 @@ func userResetPasswordCmd(args []string) { requireString(fs, *email, "email") requireString(fs, *password, "password") - a, cleanup := createApp(fs, *dbPath) + a, cleanup := setupAppWithDB(fs, *dbPath) defer cleanup() // Find the user @@ -151,27 +151,6 @@ func userResetPasswordCmd(args []string) { fmt.Printf("Email: %s\n", *email) } -func userListCmd(args []string, output io.Writer) { - fs := setupFlagSet("list", "dnote-server user list") - - dbPath := fs.String("dbPath", "", "Path to SQLite database file (env: DBPath, default: $XDG_DATA_HOME/dnote/server.db)") - - fs.Parse(args) - - a, cleanup := createApp(fs, *dbPath) - defer cleanup() - - users, err := a.GetAllUsers() - if err != nil { - log.ErrorWrap(err, "listing users") - os.Exit(1) - } - - for _, user := range users { - fmt.Fprintf(output, "%s,%s,%s\n", user.UUID, user.Email.String, user.CreatedAt.UTC().Format("2006-01-02T15:04:05Z")) - } -} - func userCmd(args []string) { if len(args) < 1 { fmt.Println(`Usage: @@ -179,7 +158,6 @@ func userCmd(args []string) { Available commands: create: Create a new user - list: List all users remove: Remove a user reset-password: Reset a user's password`) os.Exit(1) @@ -194,8 +172,6 @@ Available commands: switch subcommand { case "create": userCreateCmd(subArgs) - case "list": - userListCmd(subArgs, os.Stdout) case "remove": userRemoveCmd(subArgs, os.Stdin) case "reset-password": @@ -204,7 +180,6 @@ Available commands: fmt.Printf("Unknown subcommand: %s\n\n", subcommand) fmt.Println(`Available commands: create: Create a new user - list: List all users remove: Remove a user (only if they have no notes or books) reset-password: Reset a user's password`) os.Exit(1) diff --git a/pkg/server/cmd/user_test.go b/pkg/server/cmd/user_test.go index 84e5f4de..834ae3bd 100644 --- a/pkg/server/cmd/user_test.go +++ b/pkg/server/cmd/user_test.go @@ -16,8 +16,6 @@ package cmd import ( - "bytes" - "fmt" "strings" "testing" @@ -109,50 +107,3 @@ func TestUserResetPasswordCmd(t *testing.T) { err = bcrypt.CompareHashAndPassword([]byte(updatedUser.Password.String), []byte("oldpassword123")) assert.Equal(t, err != nil, true, "old password should not match") } - -func TestUserListCmd(t *testing.T) { - t.Run("multiple users", func(t *testing.T) { - tmpDB := t.TempDir() + "/test.db" - - // Create multiple users - db := testutils.InitDB(tmpDB) - user1 := testutils.SetupUserData(db, "alice@example.com", "password123") - user2 := testutils.SetupUserData(db, "bob@example.com", "password123") - user3 := testutils.SetupUserData(db, "charlie@example.com", "password123") - sqlDB, _ := db.DB() - sqlDB.Close() - - // Capture output - var buf bytes.Buffer - userListCmd([]string{"--dbPath", tmpDB}, &buf) - - // Verify output matches expected format - output := strings.TrimSpace(buf.String()) - lines := strings.Split(output, "\n") - - expectedLine1 := fmt.Sprintf("%s,alice@example.com,%s", user1.UUID, user1.CreatedAt.UTC().Format("2006-01-02T15:04:05Z")) - expectedLine2 := fmt.Sprintf("%s,bob@example.com,%s", user2.UUID, user2.CreatedAt.UTC().Format("2006-01-02T15:04:05Z")) - expectedLine3 := fmt.Sprintf("%s,charlie@example.com,%s", user3.UUID, user3.CreatedAt.UTC().Format("2006-01-02T15:04:05Z")) - - assert.Equal(t, lines[0], expectedLine1, "line 1 should match") - assert.Equal(t, lines[1], expectedLine2, "line 2 should match") - assert.Equal(t, lines[2], expectedLine3, "line 3 should match") - }) - - t.Run("empty database", func(t *testing.T) { - tmpDB := t.TempDir() + "/test.db" - - // Initialize empty database - db := testutils.InitDB(tmpDB) - sqlDB, _ := db.DB() - sqlDB.Close() - - // Capture output - var buf bytes.Buffer - userListCmd([]string{"--dbPath", tmpDB}, &buf) - - // Verify no output - output := buf.String() - assert.Equal(t, output, "", "should have no output for empty database") - }) -}