Compare commits

..

No commits in common. "master" and "server-v3.0.0" have entirely different histories.

23 changed files with 234 additions and 632 deletions

View file

@ -6,28 +6,27 @@ Please see the [doc](https://www.getdnote.com/docs/server) for more.
1. Install [Docker](https://docs.docker.com/install/).
2. Install Docker [Compose plugin](https://docs.docker.com/compose/install/linux/).
3. Create a `compose.yml` file with the following content:
```yaml
services:
dnote:
image: dnote/dnote:latest
container_name: dnote
ports:
- 3001:3001
volumes:
- ./dnote_data:/data
restart: unless-stopped
```
4. Run the following to download the image and start the container
3. Download the [compose.yml](https://raw.githubusercontent.com/dnote/dnote/master/host/docker/compose.yml) file by running:
```
curl https://raw.githubusercontent.com/dnote/dnote/master/host/docker/compose.yml > compose.yml
```
4. Run the following to download the images and run the containers
```
docker compose pull
docker compose up -d
```
Visit http://localhost:3001 in your browser to see Dnote running.
### Supported platform
Currently, the official Docker image for Dnote supports Linux running AMD64 CPU architecture.
If you run ARM64, please install Dnote server by downloading a binary distribution (see below).
## Manual Installation
Download from [releases](https://github.com/dnote/dnote/releases), extract, and run:
@ -35,7 +34,7 @@ Download from [releases](https://github.com/dnote/dnote/releases), extract, and
```bash
tar -xzf dnote-server-$version-$os.tar.gz
mv ./dnote-server /usr/local/bin
dnote-server start --baseUrl=https://your.server
dnote-server start --webUrl=https://your.server
```
You're up and running. Database: `~/.local/share/dnote/server.db` (customize with `--dbPath`). Run `dnote-server start --help` for options.

9
go.mod
View file

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

18
go.sum
View file

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

View file

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

95
pkg/cli/cmd/cat/cat.go Normal file
View file

@ -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 <book name> <note index>",
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
}
}

View file

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

View file

@ -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 <book name?>",
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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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