freshjots 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a9a2097c87005bc6e9b43c35881949e54bce2871962789f463ed347b4e2e415
4
- data.tar.gz: 18f981d7b062f4e683b0c4bb4331f6be9442e94cad3f4067dea8847ed9d3893e
3
+ metadata.gz: 3916dec8cefe696476c18a0a28aba36a1657721082098eef1929129c05355b84
4
+ data.tar.gz: 94b852d213f92017d897fe309efd21f9ecf60c6d398e0cbfe6c37af0f11219b0
5
5
  SHA512:
6
- metadata.gz: 00b9d55134d2826c0aa1515277907e31bcd1cb9b0df62918e194ca4b7f7e2542008fb3236b5503cb815ebde47702e4bac1e094a6ba3d2a5ac025ebb5ebf89171
7
- data.tar.gz: f5f7dbcc724409cdb3b969972e0c5ff455abc715f3fc0da6650eaf1b6c654693ebd3a69c93c8f2f67aff021b529c41b37f90b506f3ead3d1e059737bfc4c4b85
6
+ metadata.gz: 8c650cb3dfdb4ff2c3d4680652e9fa8ec82cfd2fa5f6605fe9e33a3f67b3b6149d46ac5b3f6aada3cb2b3a7c4c8e77d2e4ebf98c122a5957439ce2205001aa9b
7
+ data.tar.gz: 1ee7ec3cace2bf20ce328f27d9ffb1ed91348eabb5fc3bf1354a9c21f09cd06707d22ca8f9beeb8dfc42b23a39bf8c873d19c0d7bc48af8089f149d659ede7e9
data/README.md CHANGED
@@ -44,13 +44,25 @@ end
44
44
  created = client.create(title: "Research 2026 Q2", body: "Initial outline.")
45
45
  puts created[:filename] # server-derived stream name
46
46
 
47
- # Organize: move into a folder (by id or name), delete (by id or filename), list folders.
47
+ # Update a note's fields (only the keys you pass change). By id or by filename:
48
+ client.update(42, title: "Q2 research", body: "Revised outline.")
49
+ client.set("cron-jobs-prod", folder: "Ops", deadline: 26) # metadata only, no body needed
50
+
51
+ # Create many notes in one atomic batch (up to 50 — all land or none do).
52
+ client.bulk([{ title: "a", plain_body: "1" }, { title: "b", plain_body: "2" }])
53
+
54
+ # Organize: move into a folder (by id or name), delete (by id or filename).
48
55
  client.move("cron-jobs-prod", folder: "Ops")
49
56
  client.delete("old-note")
57
+
58
+ # Folders: list, read one, create, rename, delete (its notes survive, un-foldered).
50
59
  client.folders.each { |f| puts "#{f[:id]}\t#{f[:name]}" }
60
+ ops = client.create_folder("Ops")
61
+ client.rename_folder(ops[:id], "Operations")
62
+ client.delete_folder(ops[:id])
51
63
  ```
52
64
 
53
- Client methods: `notes(sort:, folder_id:, limit:, offset:)`, `note(filename)`, `note_by_id(id)`, `create(title:, body:, client_encrypted:)`, `append(filename, text, client_encrypted:)`, `delete(id_or_filename)`, `move(id_or_filename, folder:)`, and `folders`. Client-side crypto: `Freshjots.encrypt(text, passphrase)` / `Freshjots.decrypt(token, passphrase)` (see [Encryption](#encryption)). `note`/`note_by_id`/`create` return the note hash directly (no `{ note: … }` wrapper); `notes` and `folders` return arrays. For `notes`, `sort` is `created|updated|appended` and `folder_id` may be a folder id or `"none"` (un-foldered only).
65
+ Client methods: `notes(sort:, folder_id:, limit:, offset:)`, `note(filename)`, `note_by_id(id)`, `create(title:, body:, client_encrypted:)`, `append(filename, text, client_encrypted:)`, `update(id, **fields)`, `set(filename, **fields)`, `bulk(notes)`, `delete(id_or_filename)`, `move(id_or_filename, folder:)`, `folders`, `folder(id)`, `create_folder(name)`, `rename_folder(id, name)`, and `delete_folder(id)`. Client-side crypto: `Freshjots.encrypt(text, passphrase)` / `Freshjots.decrypt(token, passphrase)` (see [Encryption](#encryption)). `note`/`note_by_id`/`create`/`update`/`set` and the single-folder methods return the hash directly (no `{ note: … }` / `{ folder: … }` wrapper); `notes` and `folders` return arrays, and `bulk` returns `{ created: [...] }`. For `update`/`set`, the fields are `title:`, `body:`, `folder:` (id or name — `root: true` un-folders), `deadline:`, `alert_email:`, `webhook_url:`, `webhook_secret:`; changing `title:` rewrites the body as a unit, so pass `body:` too. For `notes`, `sort` is `created|updated|appended` and `folder_id` may be a folder id or `"none"` (un-foldered only).
54
66
 
55
67
  ## Encryption
56
68
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Freshjots
4
- VERSION = "1.1.0"
4
+ VERSION = "1.2.0"
5
5
  end
data/lib/freshjots.rb CHANGED
@@ -34,6 +34,11 @@ module Freshjots
34
34
 
35
35
  class Client
36
36
  DEFAULT_BASE_URL = "https://freshjots.com/api/v1"
37
+ BULK_MAX = 50
38
+ # Friendly field names accepted by #update / #set, mapped to the API's
39
+ # note keys in #note_fields. append_only / format are intentionally
40
+ # absent — the API does not update them.
41
+ UPDATABLE_FIELDS = %i[title body folder root deadline alert_email webhook_url webhook_secret].freeze
37
42
 
38
43
  def initialize(token: ENV["FRESHJOTS_TOKEN"], base_url: DEFAULT_BASE_URL)
39
44
  raise ArgumentError, "FRESHJOTS_TOKEN missing — pass token: or set the env var" if token.nil? || token.empty?
@@ -96,9 +101,37 @@ module Freshjots
96
101
  true
97
102
  end
98
103
 
104
+ # Update a note by id. Pass any of: title:, body:, folder:, root: true,
105
+ # deadline:, alert_email:, webhook_url:, webhook_secret: — only the keys
106
+ # you pass are changed, so an unmentioned field is never clobbered. A
107
+ # content change (title/body) rewrites the body as a unit, so a title
108
+ # change needs body: too (the API requires plain_body). append_only and
109
+ # format are not updatable. Returns the updated note hash (top level).
110
+ def update(id, **fields)
111
+ request(:patch, "/notes/#{escape(id)}", { note: note_fields(fields) })
112
+ end
113
+
114
+ # Update a note addressed by its exact filename / stream name. Same
115
+ # fields as #update.
116
+ def set(filename, **fields)
117
+ request(:patch, "/notes/by-filename/#{escape(filename)}", { note: note_fields(fields) })
118
+ end
119
+
120
+ # Create up to 50 notes in one atomic batch (all land or none do).
121
+ # `notes` is an array of note hashes ({ title:, plain_body:,
122
+ # format: "plain" }). Returns the response ({ created: [...] }).
123
+ def bulk(notes)
124
+ items = Array(notes)
125
+ raise ArgumentError, "bulk requires at least one note" if items.empty?
126
+ raise ArgumentError, "bulk accepts at most #{BULK_MAX} notes (got #{items.size})" if items.size > BULK_MAX
127
+
128
+ request(:post, "/notes/bulk", { notes: items })
129
+ end
130
+
99
131
  # Delete a note. Accepts a numeric id or a filename (resolved to its
100
- # id via the by-filename lookup). Locked (append-only) notes are
101
- # refused by the API with note_locked. Returns true on success.
132
+ # id via the by-filename lookup). Works on any note, including locked
133
+ # (append-only) ones the lock freezes content, not deletability.
134
+ # Returns true on success.
102
135
  def delete(id_or_filename)
103
136
  request(:delete, "/notes/#{resolve_note_id(id_or_filename)}")
104
137
  true
@@ -117,6 +150,28 @@ module Freshjots
117
150
  request(:get, "/folders")[:folders]
118
151
  end
119
152
 
153
+ # Fetch one folder by id (GET /folders/:id) — top-level serializer.
154
+ def folder(id)
155
+ request(:get, "/folders/#{escape(id)}")
156
+ end
157
+
158
+ # Create a folder. Returns the created folder hash (top level).
159
+ def create_folder(name)
160
+ request(:post, "/folders", { folder: { name: name } })
161
+ end
162
+
163
+ # Rename a folder. Returns the updated folder hash.
164
+ def rename_folder(id, name)
165
+ request(:patch, "/folders/#{escape(id)}", { folder: { name: name } })
166
+ end
167
+
168
+ # Delete a folder by id. Its notes survive — they just become
169
+ # un-foldered. Returns true.
170
+ def delete_folder(id)
171
+ request(:delete, "/folders/#{escape(id)}")
172
+ true
173
+ end
174
+
120
175
  private
121
176
 
122
177
  # A note reference is either a numeric id (used as-is) or a
@@ -140,6 +195,42 @@ module Freshjots
140
195
  matches.first[:id]
141
196
  end
142
197
 
198
+ # Map the friendly #update / #set keyword fields to the API's note keys,
199
+ # sending only what the caller passed. A title change rewrites the body
200
+ # as a unit (the API requires plain_body), so a title-only change is
201
+ # refused here — mirrors the CLI. Unknown fields and an empty change are
202
+ # errors. `folder:` accepts an id or a name (resolved via /folders);
203
+ # `root: true` un-folders the note.
204
+ def note_fields(fields)
205
+ unknown = fields.keys - UPDATABLE_FIELDS
206
+ unless unknown.empty?
207
+ raise ArgumentError,
208
+ "unknown update field(s): #{unknown.join(', ')}. " \
209
+ "allowed: #{UPDATABLE_FIELDS.join(', ')} (append_only/format are not updatable)"
210
+ end
211
+ if fields.key?(:title) && !fields.key?(:body)
212
+ raise ArgumentError,
213
+ "changing the title also rewrites the body — pass body: too. For metadata " \
214
+ "only, use folder/root/deadline/alert_email/webhook_* without title."
215
+ end
216
+
217
+ note = {}
218
+ note[:title] = fields[:title] if fields.key?(:title)
219
+ note[:plain_body] = fields[:body] if fields.key?(:body)
220
+ if fields[:root]
221
+ note[:folder_id] = nil
222
+ elsif fields.key?(:folder)
223
+ note[:folder_id] = resolve_folder_id(fields[:folder])
224
+ end
225
+ note[:append_deadline_hours] = fields[:deadline] if fields.key?(:deadline)
226
+ note[:alert_email] = fields[:alert_email] if fields.key?(:alert_email)
227
+ note[:webhook_url] = fields[:webhook_url] if fields.key?(:webhook_url)
228
+ note[:webhook_secret] = fields[:webhook_secret] if fields.key?(:webhook_secret)
229
+ raise ArgumentError, "no fields to update" if note.empty?
230
+
231
+ note
232
+ end
233
+
143
234
  def request(method, path, body = nil)
144
235
  uri = URI("#{@base_url}#{path}")
145
236
  req = Net::HTTP.const_get(method.to_s.capitalize).new(uri)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: freshjots
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Goran Arsov