apps/jotts/tui/backend.go 4.9 K raw
1
package tui
2
3
import (
4
	"bytes"
5
	"database/sql"
6
	"encoding/json"
7
	"fmt"
8
	"io"
9
	"net/http"
10
	"os"
11
	"strings"
12
	"time"
13
14
	"github.com/stevedylandev/andromeda/apps/jotts/internal/store"
15
	"github.com/stevedylandev/andromeda/pkg/config"
16
)
17
18
type Note = store.Note
19
20
type Backend interface {
21
	List() ([]Note, error)
22
	Get(shortID string) (*Note, error)
23
	Create(title, content string) (*Note, error)
24
	Update(shortID, title, content string) (*Note, error)
25
	Delete(shortID string) (bool, error)
26
	RemoteURL() string
27
	Close() error
28
}
29
30
type LocalBackend struct {
31
	DB *sql.DB
32
}
33
34
func (b *LocalBackend) List() ([]Note, error)             { return store.List(b.DB) }
35
func (b *LocalBackend) Get(s string) (*Note, error)       { return store.GetByShortID(b.DB, s) }
36
func (b *LocalBackend) Create(t, c string) (*Note, error) { return store.Create(b.DB, t, c) }
37
func (b *LocalBackend) Update(s, t, c string) (*Note, error) {
38
	return store.UpdateByShortID(b.DB, s, t, c)
39
}
40
func (b *LocalBackend) Delete(s string) (bool, error) { return store.DeleteByShortID(b.DB, s) }
41
func (b *LocalBackend) RemoteURL() string             { return "" }
42
func (b *LocalBackend) Close() error                  { return b.DB.Close() }
43
44
type RemoteBackend struct {
45
	BaseURL string
46
	APIKey  string
47
	Client  *http.Client
48
}
49
50
func (r *RemoteBackend) RemoteURL() string { return r.BaseURL }
51
func (r *RemoteBackend) Close() error      { return nil }
52
53
func (r *RemoteBackend) do(method, path string, body any, out any) error {
54
	var reader io.Reader
55
	if body != nil {
56
		buf, err := json.Marshal(body)
57
		if err != nil {
58
			return err
59
		}
60
		reader = bytes.NewReader(buf)
61
	}
62
	req, err := http.NewRequest(method, strings.TrimRight(r.BaseURL, "/")+path, reader)
63
	if err != nil {
64
		return err
65
	}
66
	if body != nil {
67
		req.Header.Set("Content-Type", "application/json")
68
	}
69
	if r.APIKey != "" {
70
		req.Header.Set("x-api-key", r.APIKey)
71
	}
72
	resp, err := r.Client.Do(req)
73
	if err != nil {
74
		return err
75
	}
76
	defer resp.Body.Close()
77
	if resp.StatusCode == http.StatusNotFound {
78
		return errNotFound
79
	}
80
	if resp.StatusCode >= 400 {
81
		b, _ := io.ReadAll(resp.Body)
82
		return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(b)))
83
	}
84
	if out == nil || resp.StatusCode == http.StatusNoContent {
85
		return nil
86
	}
87
	return json.NewDecoder(resp.Body).Decode(out)
88
}
89
90
var errNotFound = fmt.Errorf("not found")
91
92
func (r *RemoteBackend) List() ([]Note, error) {
93
	var out []Note
94
	if err := r.do("GET", "/api/notes", nil, &out); err != nil {
95
		return nil, err
96
	}
97
	return out, nil
98
}
99
100
func (r *RemoteBackend) Get(shortID string) (*Note, error) {
101
	var n Note
102
	if err := r.do("GET", "/api/notes/"+shortID, nil, &n); err != nil {
103
		if err == errNotFound {
104
			return nil, nil
105
		}
106
		return nil, err
107
	}
108
	return &n, nil
109
}
110
111
func (r *RemoteBackend) Create(title, content string) (*Note, error) {
112
	var n Note
113
	if err := r.do("POST", "/api/notes", store.NoteInput{Title: title, Content: content}, &n); err != nil {
114
		return nil, err
115
	}
116
	return &n, nil
117
}
118
119
func (r *RemoteBackend) Update(shortID, title, content string) (*Note, error) {
120
	var n Note
121
	if err := r.do("PUT", "/api/notes/"+shortID, store.NoteInput{Title: title, Content: content}, &n); err != nil {
122
		if err == errNotFound {
123
			return nil, nil
124
		}
125
		return nil, err
126
	}
127
	return &n, nil
128
}
129
130
func (r *RemoteBackend) Delete(shortID string) (bool, error) {
131
	if err := r.do("DELETE", "/api/notes/"+shortID, nil, nil); err != nil {
132
		if err == errNotFound {
133
			return false, nil
134
		}
135
		return false, err
136
	}
137
	return true, nil
138
}
139
140
type Options struct {
141
	RemoteURL string
142
	APIKey    string
143
	DBPath    string
144
}
145
146
func ParseArgs(args []string) Options {
147
	opts := Options{}
148
	for i := 0; i < len(args); i++ {
149
		a := args[i]
150
		switch {
151
		case a == "--remote" && i+1 < len(args):
152
			opts.RemoteURL = args[i+1]
153
			i++
154
		case strings.HasPrefix(a, "--remote="):
155
			opts.RemoteURL = strings.TrimPrefix(a, "--remote=")
156
		case a == "--api-key" && i+1 < len(args):
157
			opts.APIKey = args[i+1]
158
			i++
159
		case strings.HasPrefix(a, "--api-key="):
160
			opts.APIKey = strings.TrimPrefix(a, "--api-key=")
161
		case a == "--db" && i+1 < len(args):
162
			opts.DBPath = args[i+1]
163
			i++
164
		}
165
	}
166
	return opts
167
}
168
169
func ResolveBackend(opts Options) (Backend, error) {
170
	cfg, _ := LoadConfig()
171
172
	remoteURL := opts.RemoteURL
173
	explicitRemote := remoteURL != ""
174
	if remoteURL == "" {
175
		remoteURL = os.Getenv("JOTTS_REMOTE_URL")
176
	}
177
	if remoteURL == "" {
178
		remoteURL = cfg.RemoteURL
179
	}
180
181
	apiKey := opts.APIKey
182
	if apiKey == "" {
183
		apiKey = os.Getenv("JOTTS_API_KEY")
184
	}
185
	if apiKey == "" {
186
		apiKey = cfg.APIKey
187
	}
188
189
	dbPath := opts.DBPath
190
	explicitDB := dbPath != ""
191
	if dbPath == "" {
192
		dbPath = config.Getenv("JOTTS_DB_PATH", "jotts.sqlite")
193
	}
194
195
	useRemote := remoteURL != "" && (!explicitDB || explicitRemote)
196
197
	if useRemote {
198
		return &RemoteBackend{
199
			BaseURL: remoteURL,
200
			APIKey:  apiKey,
201
			Client:  &http.Client{Timeout: 15 * time.Second},
202
		}, nil
203
	}
204
205
	db, err := store.Open(dbPath)
206
	if err != nil {
207
		return nil, err
208
	}
209
	return &LocalBackend{DB: db}, nil
210
}