@37signals/basecamp 0.12.0 → 0.14.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.
Files changed (294) hide show
  1. package/README.md +114 -17
  2. package/dist/.build-complete +0 -0
  3. package/dist/client.d.ts +53 -11
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +132 -150
  6. package/dist/client.js.map +1 -1
  7. package/dist/download.d.ts +7 -0
  8. package/dist/download.d.ts.map +1 -1
  9. package/dist/download.js +82 -22
  10. package/dist/download.js.map +1 -1
  11. package/dist/errors.d.ts +19 -1
  12. package/dist/errors.d.ts.map +1 -1
  13. package/dist/errors.js +138 -14
  14. package/dist/errors.js.map +1 -1
  15. package/dist/generated/metadata.d.ts.map +1 -1
  16. package/dist/generated/metadata.js +291 -141
  17. package/dist/generated/metadata.js.map +1 -1
  18. package/dist/generated/metadata.ts +291 -141
  19. package/dist/generated/openapi-stripped.json +5954 -3523
  20. package/dist/generated/path-mapping.d.ts.map +1 -1
  21. package/dist/generated/path-mapping.js +28 -16
  22. package/dist/generated/path-mapping.js.map +1 -1
  23. package/dist/generated/path-mapping.ts +28 -16
  24. package/dist/generated/schema.d.ts +4390 -2357
  25. package/dist/generated/services/bookmarks.d.ts +1 -1
  26. package/dist/generated/services/bookmarks.d.ts.map +1 -1
  27. package/dist/generated/services/bookmarks.ts +1 -1
  28. package/dist/generated/services/boosts.d.ts +10 -0
  29. package/dist/generated/services/boosts.d.ts.map +1 -1
  30. package/dist/generated/services/boosts.js +8 -0
  31. package/dist/generated/services/boosts.js.map +1 -1
  32. package/dist/generated/services/boosts.ts +12 -0
  33. package/dist/generated/services/campfires.d.ts +54 -40
  34. package/dist/generated/services/campfires.d.ts.map +1 -1
  35. package/dist/generated/services/campfires.js +82 -65
  36. package/dist/generated/services/campfires.js.map +1 -1
  37. package/dist/generated/services/campfires.ts +107 -85
  38. package/dist/generated/services/cards.d.ts +5 -0
  39. package/dist/generated/services/cards.d.ts.map +1 -1
  40. package/dist/generated/services/cards.js +4 -0
  41. package/dist/generated/services/cards.js.map +1 -1
  42. package/dist/generated/services/cards.ts +6 -0
  43. package/dist/generated/services/checkins.d.ts +14 -0
  44. package/dist/generated/services/checkins.d.ts.map +1 -1
  45. package/dist/generated/services/checkins.js +14 -1
  46. package/dist/generated/services/checkins.js.map +1 -1
  47. package/dist/generated/services/checkins.ts +20 -0
  48. package/dist/generated/services/client-approvals.d.ts +6 -3
  49. package/dist/generated/services/client-approvals.d.ts.map +1 -1
  50. package/dist/generated/services/client-approvals.js +8 -5
  51. package/dist/generated/services/client-approvals.js.map +1 -1
  52. package/dist/generated/services/client-approvals.ts +10 -5
  53. package/dist/generated/services/client-correspondences.d.ts +6 -3
  54. package/dist/generated/services/client-correspondences.d.ts.map +1 -1
  55. package/dist/generated/services/client-correspondences.js +8 -5
  56. package/dist/generated/services/client-correspondences.js.map +1 -1
  57. package/dist/generated/services/client-correspondences.ts +10 -5
  58. package/dist/generated/services/client-replies.d.ts +11 -4
  59. package/dist/generated/services/client-replies.d.ts.map +1 -1
  60. package/dist/generated/services/client-replies.js +16 -8
  61. package/dist/generated/services/client-replies.js.map +1 -1
  62. package/dist/generated/services/client-replies.ts +18 -8
  63. package/dist/generated/services/cloud-files.d.ts +92 -0
  64. package/dist/generated/services/cloud-files.d.ts.map +1 -0
  65. package/dist/generated/services/cloud-files.js +121 -0
  66. package/dist/generated/services/cloud-files.js.map +1 -0
  67. package/dist/generated/services/cloud-files.ts +188 -0
  68. package/dist/generated/services/comments.d.ts +5 -0
  69. package/dist/generated/services/comments.d.ts.map +1 -1
  70. package/dist/generated/services/comments.js +4 -0
  71. package/dist/generated/services/comments.js.map +1 -1
  72. package/dist/generated/services/comments.ts +6 -0
  73. package/dist/generated/services/documents.d.ts +12 -7
  74. package/dist/generated/services/documents.d.ts.map +1 -1
  75. package/dist/generated/services/documents.js +10 -6
  76. package/dist/generated/services/documents.js.map +1 -1
  77. package/dist/generated/services/documents.ts +14 -8
  78. package/dist/generated/services/drafts.d.ts +1 -1
  79. package/dist/generated/services/drafts.d.ts.map +1 -1
  80. package/dist/generated/services/drafts.ts +1 -1
  81. package/dist/generated/services/events.d.ts +5 -0
  82. package/dist/generated/services/events.d.ts.map +1 -1
  83. package/dist/generated/services/events.js +4 -0
  84. package/dist/generated/services/events.js.map +1 -1
  85. package/dist/generated/services/events.ts +6 -0
  86. package/dist/generated/services/everything.d.ts +14 -14
  87. package/dist/generated/services/everything.d.ts.map +1 -1
  88. package/dist/generated/services/everything.ts +14 -14
  89. package/dist/generated/services/folders.d.ts +96 -0
  90. package/dist/generated/services/folders.d.ts.map +1 -0
  91. package/dist/generated/services/folders.js +140 -0
  92. package/dist/generated/services/folders.js.map +1 -0
  93. package/dist/generated/services/folders.ts +201 -0
  94. package/dist/generated/services/forwards.d.ts +7 -20
  95. package/dist/generated/services/forwards.d.ts.map +1 -1
  96. package/dist/generated/services/forwards.js +6 -35
  97. package/dist/generated/services/forwards.js.map +1 -1
  98. package/dist/generated/services/forwards.ts +10 -48
  99. package/dist/generated/services/gauges.d.ts +7 -0
  100. package/dist/generated/services/gauges.d.ts.map +1 -1
  101. package/dist/generated/services/gauges.js +5 -1
  102. package/dist/generated/services/gauges.js.map +1 -1
  103. package/dist/generated/services/gauges.ts +9 -1
  104. package/dist/generated/services/google-documents.d.ts +98 -0
  105. package/dist/generated/services/google-documents.d.ts.map +1 -0
  106. package/dist/generated/services/google-documents.js +123 -0
  107. package/dist/generated/services/google-documents.js.map +1 -0
  108. package/dist/generated/services/google-documents.ts +196 -0
  109. package/dist/generated/services/index.d.ts +7 -4
  110. package/dist/generated/services/index.d.ts.map +1 -1
  111. package/dist/generated/services/index.js +7 -4
  112. package/dist/generated/services/index.js.map +1 -1
  113. package/dist/generated/services/index.ts +7 -4
  114. package/dist/generated/services/messages.d.ts +2 -0
  115. package/dist/generated/services/messages.d.ts.map +1 -1
  116. package/dist/generated/services/messages.js +1 -1
  117. package/dist/generated/services/messages.js.map +1 -1
  118. package/dist/generated/services/messages.ts +3 -1
  119. package/dist/generated/services/my-notes.d.ts +1 -1
  120. package/dist/generated/services/my-notes.js +1 -1
  121. package/dist/generated/services/my-notes.ts +1 -1
  122. package/dist/generated/services/my-notifications.d.ts +4 -2
  123. package/dist/generated/services/my-notifications.d.ts.map +1 -1
  124. package/dist/generated/services/my-notifications.js.map +1 -1
  125. package/dist/generated/services/my-notifications.ts +4 -2
  126. package/dist/generated/services/people.d.ts +11 -1
  127. package/dist/generated/services/people.d.ts.map +1 -1
  128. package/dist/generated/services/people.js +13 -2
  129. package/dist/generated/services/people.js.map +1 -1
  130. package/dist/generated/services/people.ts +15 -1
  131. package/dist/generated/services/projects.d.ts +26 -0
  132. package/dist/generated/services/projects.d.ts.map +1 -1
  133. package/dist/generated/services/projects.js +49 -1
  134. package/dist/generated/services/projects.js.map +1 -1
  135. package/dist/generated/services/projects.ts +61 -1
  136. package/dist/generated/services/recordings.d.ts +2 -12
  137. package/dist/generated/services/recordings.d.ts.map +1 -1
  138. package/dist/generated/services/recordings.js +1 -26
  139. package/dist/generated/services/recordings.js.map +1 -1
  140. package/dist/generated/services/recordings.ts +3 -31
  141. package/dist/generated/services/reports.d.ts +8 -12
  142. package/dist/generated/services/reports.d.ts.map +1 -1
  143. package/dist/generated/services/reports.js +11 -5
  144. package/dist/generated/services/reports.js.map +1 -1
  145. package/dist/generated/services/reports.ts +13 -14
  146. package/dist/generated/services/schedules.d.ts +94 -15
  147. package/dist/generated/services/schedules.d.ts.map +1 -1
  148. package/dist/generated/services/schedules.js +19 -8
  149. package/dist/generated/services/schedules.js.map +1 -1
  150. package/dist/generated/services/schedules.ts +107 -17
  151. package/dist/generated/services/search.d.ts +2 -0
  152. package/dist/generated/services/search.d.ts.map +1 -1
  153. package/dist/generated/services/search.js +1 -1
  154. package/dist/generated/services/search.js.map +1 -1
  155. package/dist/generated/services/search.ts +3 -1
  156. package/dist/generated/services/templates.d.ts +2 -0
  157. package/dist/generated/services/templates.d.ts.map +1 -1
  158. package/dist/generated/services/templates.js +1 -1
  159. package/dist/generated/services/templates.js.map +1 -1
  160. package/dist/generated/services/templates.ts +3 -1
  161. package/dist/generated/services/timeline.d.ts +2 -0
  162. package/dist/generated/services/timeline.d.ts.map +1 -1
  163. package/dist/generated/services/timeline.js +1 -0
  164. package/dist/generated/services/timeline.js.map +1 -1
  165. package/dist/generated/services/timeline.ts +3 -0
  166. package/dist/generated/services/timesheets.d.ts +16 -0
  167. package/dist/generated/services/timesheets.d.ts.map +1 -1
  168. package/dist/generated/services/timesheets.js +26 -2
  169. package/dist/generated/services/timesheets.js.map +1 -1
  170. package/dist/generated/services/timesheets.ts +35 -2
  171. package/dist/generated/services/todolist-groups.d.ts +11 -6
  172. package/dist/generated/services/todolist-groups.d.ts.map +1 -1
  173. package/dist/generated/services/todolist-groups.js +7 -3
  174. package/dist/generated/services/todolist-groups.js.map +1 -1
  175. package/dist/generated/services/todolist-groups.ts +13 -7
  176. package/dist/generated/services/todolists.d.ts +15 -13
  177. package/dist/generated/services/todolists.d.ts.map +1 -1
  178. package/dist/generated/services/todolists.js +11 -8
  179. package/dist/generated/services/todolists.js.map +1 -1
  180. package/dist/generated/services/todolists.ts +19 -14
  181. package/dist/generated/services/todos.d.ts +2 -12
  182. package/dist/generated/services/todos.d.ts.map +1 -1
  183. package/dist/generated/services/todos.js +1 -25
  184. package/dist/generated/services/todos.js.map +1 -1
  185. package/dist/generated/services/todos.ts +3 -30
  186. package/dist/generated/services/uploads.d.ts +41 -2
  187. package/dist/generated/services/uploads.d.ts.map +1 -1
  188. package/dist/generated/services/uploads.js +41 -1
  189. package/dist/generated/services/uploads.js.map +1 -1
  190. package/dist/generated/services/uploads.ts +71 -2
  191. package/dist/generated/services/vaults.d.ts +5 -0
  192. package/dist/generated/services/vaults.d.ts.map +1 -1
  193. package/dist/generated/services/vaults.js +4 -0
  194. package/dist/generated/services/vaults.js.map +1 -1
  195. package/dist/generated/services/vaults.ts +6 -0
  196. package/dist/index.d.ts +16 -10
  197. package/dist/index.d.ts.map +1 -1
  198. package/dist/index.js +19 -8
  199. package/dist/index.js.map +1 -1
  200. package/dist/oauth/authorization-document.d.ts +201 -0
  201. package/dist/oauth/authorization-document.d.ts.map +1 -0
  202. package/dist/oauth/authorization-document.js +130 -0
  203. package/dist/oauth/authorization-document.js.map +1 -0
  204. package/dist/oauth/identity.d.ts +7 -1
  205. package/dist/oauth/identity.d.ts.map +1 -1
  206. package/dist/oauth/identity.js +8 -19
  207. package/dist/oauth/identity.js.map +1 -1
  208. package/dist/oauth/index.d.ts +1 -0
  209. package/dist/oauth/index.d.ts.map +1 -1
  210. package/dist/oauth/index.js +2 -0
  211. package/dist/oauth/index.js.map +1 -1
  212. package/dist/pagination-utils.d.ts +46 -0
  213. package/dist/pagination-utils.d.ts.map +1 -1
  214. package/dist/pagination-utils.js +85 -2
  215. package/dist/pagination-utils.js.map +1 -1
  216. package/dist/pagination.d.ts +11 -0
  217. package/dist/pagination.d.ts.map +1 -1
  218. package/dist/pagination.js.map +1 -1
  219. package/dist/retry.d.ts +110 -0
  220. package/dist/retry.d.ts.map +1 -0
  221. package/dist/retry.js +253 -0
  222. package/dist/retry.js.map +1 -0
  223. package/dist/services/authorization.d.ts +22 -52
  224. package/dist/services/authorization.d.ts.map +1 -1
  225. package/dist/services/authorization.js +14 -32
  226. package/dist/services/authorization.js.map +1 -1
  227. package/dist/services/base.d.ts.map +1 -1
  228. package/dist/services/base.js +59 -5
  229. package/dist/services/base.js.map +1 -1
  230. package/dist/services/cards-extensions.d.ts +26 -21
  231. package/dist/services/cards-extensions.d.ts.map +1 -1
  232. package/dist/services/cards-extensions.js +28 -31
  233. package/dist/services/cards-extensions.js.map +1 -1
  234. package/dist/services/documents-extensions.d.ts +122 -0
  235. package/dist/services/documents-extensions.d.ts.map +1 -0
  236. package/dist/services/documents-extensions.js +125 -0
  237. package/dist/services/documents-extensions.js.map +1 -0
  238. package/dist/services/merge-safe.d.ts +167 -0
  239. package/dist/services/merge-safe.d.ts.map +1 -0
  240. package/dist/services/merge-safe.js +217 -0
  241. package/dist/services/merge-safe.js.map +1 -0
  242. package/dist/services/schedules-extensions.d.ts +266 -0
  243. package/dist/services/schedules-extensions.d.ts.map +1 -0
  244. package/dist/services/schedules-extensions.js +268 -0
  245. package/dist/services/schedules-extensions.js.map +1 -0
  246. package/dist/services/todolists-extensions.d.ts +110 -0
  247. package/dist/services/todolists-extensions.d.ts.map +1 -0
  248. package/dist/services/todolists-extensions.js +262 -0
  249. package/dist/services/todolists-extensions.js.map +1 -0
  250. package/dist/services/todos-extensions.d.ts +12 -1
  251. package/dist/services/todos-extensions.d.ts.map +1 -1
  252. package/dist/services/todos-extensions.js +27 -8
  253. package/dist/services/todos-extensions.js.map +1 -1
  254. package/package.json +6 -5
  255. package/src/generated/metadata.ts +291 -141
  256. package/src/generated/openapi-stripped.json +5954 -3523
  257. package/src/generated/path-mapping.ts +28 -16
  258. package/src/generated/schema.d.ts +4390 -2357
  259. package/src/generated/services/bookmarks.ts +1 -1
  260. package/src/generated/services/boosts.ts +12 -0
  261. package/src/generated/services/campfires.ts +107 -85
  262. package/src/generated/services/cards.ts +6 -0
  263. package/src/generated/services/checkins.ts +20 -0
  264. package/src/generated/services/client-approvals.ts +10 -5
  265. package/src/generated/services/client-correspondences.ts +10 -5
  266. package/src/generated/services/client-replies.ts +18 -8
  267. package/src/generated/services/cloud-files.ts +188 -0
  268. package/src/generated/services/comments.ts +6 -0
  269. package/src/generated/services/documents.ts +14 -8
  270. package/src/generated/services/drafts.ts +1 -1
  271. package/src/generated/services/events.ts +6 -0
  272. package/src/generated/services/everything.ts +14 -14
  273. package/src/generated/services/folders.ts +201 -0
  274. package/src/generated/services/forwards.ts +10 -48
  275. package/src/generated/services/gauges.ts +9 -1
  276. package/src/generated/services/google-documents.ts +196 -0
  277. package/src/generated/services/index.ts +7 -4
  278. package/src/generated/services/messages.ts +3 -1
  279. package/src/generated/services/my-notes.ts +1 -1
  280. package/src/generated/services/my-notifications.ts +4 -2
  281. package/src/generated/services/people.ts +15 -1
  282. package/src/generated/services/projects.ts +61 -1
  283. package/src/generated/services/recordings.ts +3 -31
  284. package/src/generated/services/reports.ts +13 -14
  285. package/src/generated/services/schedules.ts +107 -17
  286. package/src/generated/services/search.ts +3 -1
  287. package/src/generated/services/templates.ts +3 -1
  288. package/src/generated/services/timeline.ts +3 -0
  289. package/src/generated/services/timesheets.ts +35 -2
  290. package/src/generated/services/todolist-groups.ts +13 -7
  291. package/src/generated/services/todolists.ts +19 -14
  292. package/src/generated/services/todos.ts +3 -30
  293. package/src/generated/services/uploads.ts +71 -2
  294. package/src/generated/services/vaults.ts +6 -0
@@ -0,0 +1,268 @@
1
+ import { SchedulesService as GeneratedSchedulesService } from "../generated/services/schedules.js";
2
+ import { requireRecord, requiredWritableBoolean, requiredWritableString, writableBoolean, writableIdList, writableString, } from "./merge-safe.js";
3
+ /** The deliberate-overwrite escape hatch named in this composite's error hints. */
4
+ const ESCAPE = "replaceEntry()";
5
+ /**
6
+ * How the guards name this record in their messages. Spelled with a space
7
+ * because {@link requireRecord} lowercases it into a sentence ("...where a
8
+ * schedule entry object was expected"); "ScheduleEntry" would read as
9
+ * "scheduleentry" there.
10
+ */
11
+ const RECORD = "Schedule entry";
12
+ const guardOptions = { record: RECORD, escape: ESCAPE };
13
+ /** The carve-out members, keyed as they appear on {@link ScheduleEntryFields}. */
14
+ const CARVE_OUTS = ["participantIds", "url", "highlighted", "notify"];
15
+ /**
16
+ * SchedulesService with merge-safe `updateEntry` and read-modify-write
17
+ * `editEntry` on top of the generated surface (`getEntry`, `replaceEntry`,
18
+ * `createEntry`, `listEntries`, ...).
19
+ *
20
+ * `PUT /{accountId}/schedule_entries/{entryId}` is a full replace:
21
+ * `Schedules::EntriesController#update` rebuilds the recordable from the
22
+ * submitted params, so a sparse PUT that omits `description` erases it and one
23
+ * that omits `all_day` turns an all-day event into a midnight-to-midnight timed
24
+ * one. Neither is a 422 — both are a `200` that quietly clears, which is what
25
+ * these composites exist to prevent.
26
+ *
27
+ * Three writable fields are exempt. BC3 seeds `participant_ids`, `url` and
28
+ * `highlighted` from the existing recordable when the request does not address
29
+ * them, so the SDK's job for those is the opposite one: keep them **off** the
30
+ * wire unless the caller asked for them.
31
+ *
32
+ * Both composites compose the public `getEntry` and `replaceEntry`, so hooks
33
+ * observe the two wire operations under their native identities
34
+ * (`GetScheduleEntry` then `ReplaceScheduleEntry`) rather than a synthetic
35
+ * composite.
36
+ *
37
+ * **Non-recurring entries only.** `ensure_non_recurring_event` 302-redirects
38
+ * both `show` and `update` for a recurring entry, and the SDK follows neither:
39
+ * the GET surfaces as an unexpected body and the PUT as a redirect. Recurrence
40
+ * itself (`recurrence_schedule`, `recurs_until`, `time_zone_name`) is unmodeled
41
+ * — BC3 forces all three to nil for a non-recurring entry.
42
+ */
43
+ export class SchedulesService extends GeneratedSchedulesService {
44
+ /**
45
+ * Sets the given fields on a schedule entry and preserves everything else:
46
+ * GETs the current entry, overlays the fields you addressed, and PUTs the
47
+ * full representation back.
48
+ *
49
+ * A key you leave off the request is not addressed. For the five full-state
50
+ * fields that means "keep what the server has"; for the four carve-outs it
51
+ * means the key never appears in the body, so BC3's own preservation applies.
52
+ * An explicitly-passed `""`, `false` or `[]` is an address and clears.
53
+ *
54
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
55
+ * concurrent write between the GET and PUT is overwritten — last write wins
56
+ * for the full-state fields. The window is one round-trip. Use `replaceEntry`
57
+ * to overwrite deliberately.
58
+ *
59
+ * @param entryId - The entry ID
60
+ * @param req - Fields to set; unaddressed fields are preserved
61
+ * @returns The updated ScheduleEntry
62
+ * @throws {BasecampError} If the request fails, or if the read-back is malformed
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * // Rename without erasing the description, times or all-day flag.
67
+ * await client.schedules.updateEntry(123, { summary: "Team Meeting & Kickoff" });
68
+ *
69
+ * // Drop the join link and the highlight; participants are left alone.
70
+ * await client.schedules.updateEntry(123, { url: "", highlighted: false });
71
+ * ```
72
+ */
73
+ async updateEntry(entryId, req) {
74
+ const current = await this.currentState(entryId);
75
+ const addressed = (key) => Object.prototype.hasOwnProperty.call(req, key);
76
+ // Full state: always sent. `??` covers the one degenerate presence case —
77
+ // a key set to `undefined` (or, from untyped JS, `null`). Neither can ride
78
+ // the wire, so honouring it as an address would drop the key and hand BC3
79
+ // the clear-by-omission this composite exists to prevent; the read-back
80
+ // value is the answer instead.
81
+ const state = {
82
+ summary: addressed("summary") ? (req.summary ?? current.summary) : current.summary,
83
+ startsAt: addressed("startsAt") ? (req.startsAt ?? current.startsAt) : current.startsAt,
84
+ endsAt: addressed("endsAt") ? (req.endsAt ?? current.endsAt) : current.endsAt,
85
+ description: addressed("description")
86
+ ? (req.description ?? current.description)
87
+ : current.description,
88
+ allDay: addressed("allDay") ? (req.allDay ?? current.allDay) : current.allDay,
89
+ };
90
+ // Addressed-only: never seeded, so presence alone decides. `false`, `""`
91
+ // and `[]` all pass, which is the point — they are clears, and BC3 would
92
+ // preserve rather than clear if the key went missing.
93
+ const carveOuts = {};
94
+ if (addressed("participantIds"))
95
+ carveOuts.participantIds = req.participantIds;
96
+ if (addressed("url"))
97
+ carveOuts.url = req.url;
98
+ if (addressed("highlighted"))
99
+ carveOuts.highlighted = req.highlighted;
100
+ if (addressed("notify"))
101
+ carveOuts.notify = req.notify;
102
+ return this.putEntry(entryId, state, carveOuts);
103
+ }
104
+ /**
105
+ * Applies a read-modify-write callback to a schedule entry: GETs the current
106
+ * entry, hands the callback its writable state, and PUTs the whole thing
107
+ * back. If the callback throws (or rejects), the edit aborts and nothing is
108
+ * written.
109
+ *
110
+ * The five full-state fields always ride back, so clearing one means setting
111
+ * it empty and an untouched one keeps its current value. The four carve-outs
112
+ * (`participantIds`, `url`, `highlighted`, `notify`) are seeded so you can
113
+ * read them, but they reach the wire only if the callback **assigns** to
114
+ * them. Assignment is tracked by invocation, not by comparing values:
115
+ * `e.url = e.url` is a write, because a caller who reassigns the value the
116
+ * read returned still means "write this", and no diff can recover that
117
+ * intent.
118
+ *
119
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
120
+ * concurrent write between the GET and PUT is overwritten — last write wins
121
+ * for the full-state fields. The window is one round-trip. Use `replaceEntry`
122
+ * to overwrite deliberately.
123
+ *
124
+ * @param entryId - The entry ID
125
+ * @param fn - Callback that mutates the entry's writable fields in place
126
+ * @returns The updated ScheduleEntry
127
+ * @throws {BasecampError} If the request fails, or if the read-back is malformed
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * await client.schedules.editEntry(123, (e) => {
132
+ * e.summary = `🚨 ${e.summary}`;
133
+ * e.description = ""; // clearing = setting empty on a full object
134
+ * // participants, join link and highlight are untouched, so they stay off
135
+ * // the wire and BC3 keeps them.
136
+ * });
137
+ * ```
138
+ */
139
+ async editEntry(entryId, fn) {
140
+ const fields = await this.currentFields(entryId);
141
+ // Setter-invocation dirty tracking. A snapshot diff would be wrong, not
142
+ // merely different: the callback that assigns exactly what the GET returned
143
+ // has still asked for a write, and dropping it hands the field back to
144
+ // BC3's preserve-on-omission.
145
+ const touched = new Set();
146
+ const view = new Proxy(fields, {
147
+ set(target, property, value) {
148
+ if (typeof property === "string")
149
+ touched.add(property);
150
+ return Reflect.set(target, property, value);
151
+ },
152
+ });
153
+ await fn(view);
154
+ const carveOuts = {};
155
+ if (touched.has("participantIds"))
156
+ carveOuts.participantIds = fields.participantIds;
157
+ if (touched.has("url"))
158
+ carveOuts.url = fields.url;
159
+ if (touched.has("highlighted"))
160
+ carveOuts.highlighted = fields.highlighted;
161
+ if (touched.has("notify"))
162
+ carveOuts.notify = fields.notify;
163
+ return this.putEntry(entryId, fields, carveOuts);
164
+ }
165
+ /**
166
+ * Fetches the entry and derives the full-state half of its writable state.
167
+ *
168
+ * Every value here is resent in the full-replace PUT, so every value is
169
+ * validated before it is read. `?? ""` coalesces only `null` and `undefined`,
170
+ * leaving corruption wide open: `false`, `0`, `[]`, `{}`, `42`, `true` and
171
+ * friends would all be forwarded **verbatim** and written to the entry on a
172
+ * call that never mentioned the field. Nothing below this rejects them —
173
+ * `schema.d.ts` is erased at build time, so `ScheduleEntry` is a compile-time
174
+ * claim about runtime data, which puts this composite with Python and Ruby
175
+ * rather than with Go and Swift. See `merge-safe.ts` and #576.
176
+ *
177
+ * The guards differ per field because the spec models the fields
178
+ * differently:
179
+ *
180
+ * - `summary` is `@required` and `Schedule::Entry#summary` is
181
+ * `super.presence || "Untitled"`, so blank is impossible from a healthy
182
+ * server and absent/null/blank is malformed.
183
+ * - `starts_at`/`ends_at` are `@required` and `NOT NULL`. The value is a bare
184
+ * date for an all-day entry and a timestamp otherwise; it round-trips
185
+ * verbatim rather than through a `Date`, which would normalise the two
186
+ * spellings into one and rewrite the entry's shape.
187
+ * - `all_day` is `@required`, `NOT NULL DEFAULT false`. It needs the boolean
188
+ * guard specifically because a truthiness test would read the legitimate
189
+ * `false` as missing — and defaulting a missing value to `false` converts
190
+ * an all-day event into a midnight-to-midnight timed one.
191
+ * - `description` is optional and nullable both ways, so absent or null is a
192
+ * genuinely empty description.
193
+ */
194
+ async currentState(entryId) {
195
+ const current = await this.readEntry(entryId);
196
+ return {
197
+ summary: requiredWritableString(current, "summary", guardOptions),
198
+ startsAt: requiredWritableString(current, "starts_at", guardOptions),
199
+ endsAt: requiredWritableString(current, "ends_at", guardOptions),
200
+ description: writableString(current, "description", guardOptions),
201
+ allDay: requiredWritableBoolean(current, "all_day", guardOptions),
202
+ };
203
+ }
204
+ /**
205
+ * The full-state fields plus the carve-out seeds the `editEntry` callback can
206
+ * read.
207
+ *
208
+ * `url` is seeded from the response's **`join_url`**, not its `url`: the
209
+ * latter is the entry's own Basecamp API URL, written by
210
+ * `recordings/_recording` before the entry partial renders. Echoing it into
211
+ * the request's `url` would write the API URL into the join link.
212
+ *
213
+ * `updateEntry` deliberately does not come through here. It never seeds a
214
+ * carve-out, so validating `participants` for it would refuse a write that
215
+ * does not depend on them.
216
+ */
217
+ async currentFields(entryId) {
218
+ const current = await this.readEntry(entryId);
219
+ return {
220
+ summary: requiredWritableString(current, "summary", guardOptions),
221
+ startsAt: requiredWritableString(current, "starts_at", guardOptions),
222
+ endsAt: requiredWritableString(current, "ends_at", guardOptions),
223
+ description: writableString(current, "description", guardOptions),
224
+ allDay: requiredWritableBoolean(current, "all_day", guardOptions),
225
+ participantIds: writableIdList(current, "participants", guardOptions),
226
+ url: writableString(current, "join_url", guardOptions),
227
+ highlighted: writableBoolean(current, "highlighted", guardOptions),
228
+ };
229
+ }
230
+ /**
231
+ * GETs the entry and checks the envelope before any field is read.
232
+ *
233
+ * A successful GET can still return a scalar, an array or null; reading a
234
+ * property off `null` throws a raw `TypeError` instead of the documented
235
+ * statusless `api_error`, and reading one off an array or string silently
236
+ * yields `undefined`, which the field guards would then take at face value.
237
+ */
238
+ async readEntry(entryId) {
239
+ return requireRecord(await this.getEntry(entryId), {
240
+ record: RECORD,
241
+ operation: "GetScheduleEntry",
242
+ escape: ESCAPE,
243
+ });
244
+ }
245
+ /**
246
+ * PUTs the full writable state via `replaceEntry`, plus whichever carve-outs
247
+ * the caller addressed.
248
+ *
249
+ * The five full-state fields are always named, empties included: on a
250
+ * full-replace endpoint `""` is how a clear is expressed — never JSON null
251
+ * (SPEC §18 body compaction), and never by omission, which would leave the
252
+ * field to BC3's clear-by-default and read as an accident rather than an
253
+ * intent. The carve-outs are the mirror image: an unaddressed one is absent
254
+ * from `carveOuts`, so the generated request builder leaves it `undefined`
255
+ * and it never reaches the wire.
256
+ */
257
+ putEntry(entryId, state, carveOuts) {
258
+ return this.replaceEntry(entryId, {
259
+ summary: state.summary,
260
+ startsAt: state.startsAt,
261
+ endsAt: state.endsAt,
262
+ description: state.description,
263
+ allDay: state.allDay,
264
+ ...carveOuts,
265
+ });
266
+ }
267
+ }
268
+ //# sourceMappingURL=schedules-extensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedules-extensions.js","sourceRoot":"","sources":["../../src/services/schedules-extensions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,110 @@
1
+ import { TodolistsService as GeneratedTodolistsService } from "../generated/services/todolists.js";
2
+ import type { Todolist } from "../generated/services/todolists.js";
3
+ /**
4
+ * Request parameters for update. Both fields are optional: an omitted field
5
+ * is left untouched on the todolist, guaranteed. An explicitly-passed empty
6
+ * string is a set (clears the description).
7
+ */
8
+ export interface UpdateTodolistRequest {
9
+ /** Display name. Omit to leave unchanged. */
10
+ name?: string;
11
+ /** Rich text description (HTML). Omit to leave unchanged; `""` clears it. */
12
+ description?: string;
13
+ }
14
+ /**
15
+ * A todolist's full writable state, handed to the `edit` callback. The whole
16
+ * object is PUT back to the server, so clearing the description means setting
17
+ * it empty (`""`) — there is no third state.
18
+ */
19
+ export interface TodolistFields {
20
+ /** Display name (required; the server rejects a missing one with a 422). */
21
+ name: string;
22
+ /** Rich text description (HTML). Set `""` to clear. */
23
+ description: string;
24
+ }
25
+ /**
26
+ * TodolistsService with merge-safe `update` and read-modify-write `edit` on
27
+ * top of the generated surface (`get`, `replace`, ...).
28
+ *
29
+ * `PUT /{accountId}/todolists/{id}` is a full replace: BC3's
30
+ * `TodolistsController#update` rebuilds the recordable from only the permitted
31
+ * params, so a sparse PUT that omits `description` erases it, and one that
32
+ * omits `name` is a 422 — the attribute is presence-validated server-side, so
33
+ * omitting it is never a preserve. The writable set is exactly
34
+ * `{name, description}`.
35
+ *
36
+ * The route serves a to-do list and a to-do list group alike, and since #544
37
+ * both are the one flat `Todolist` shape — BC3's
38
+ * `todolists/groups/{index,show}.json.jbuilder` render
39
+ * `todolists/_todolist.json.jbuilder`, so a group reports `"type": "Todolist"`
40
+ * and carries `description`/`description_attachments` like any list. Nothing
41
+ * here branches on the variant; where it matters at all, discrimination is
42
+ * structural (`groups_url` for a Todoset parent, `group_position_url` for a
43
+ * Todolist parent), never the `type` string.
44
+ *
45
+ * Both methods compose the public `get` and `replace` methods, so hooks
46
+ * observe the two wire operations (`GetTodolistOrGroup` then
47
+ * `UpdateTodolistOrGroup`), not a synthetic composite.
48
+ */
49
+ export declare class TodolistsService extends GeneratedTodolistsService {
50
+ /**
51
+ * Sets the given fields on a todolist and preserves everything else: GETs
52
+ * the current todolist, overlays the explicitly-set request fields, and PUTs
53
+ * the full representation back. An omitted (`undefined`) field is untouched,
54
+ * guaranteed; an explicitly-passed `""` clears the description.
55
+ *
56
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
57
+ * concurrent write between the GET and PUT is overwritten — last write wins
58
+ * for the whole representation. The window is one round-trip. Use `replace`
59
+ * to overwrite deliberately.
60
+ *
61
+ * @param id - The todolist ID
62
+ * @param req - Fields to set; omitted fields are preserved
63
+ * @returns The updated Todolist
64
+ * @throws {BasecampError} If the request fails
65
+ *
66
+ * @example
67
+ * ```ts
68
+ * // Rename without erasing the description.
69
+ * await client.todolists.update(123, { name: "Hardware" });
70
+ * ```
71
+ */
72
+ update(id: number, req: UpdateTodolistRequest): Promise<Todolist>;
73
+ /**
74
+ * Applies a read-modify-write callback to a todolist: GETs the current
75
+ * todolist, hands the callback its full writable representation, and PUTs
76
+ * the whole thing back. Clearing the description means setting it empty
77
+ * (`""`) — an untouched field keeps its current value. If the callback
78
+ * throws (or rejects), the edit aborts and nothing is written.
79
+ *
80
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
81
+ * concurrent write between the GET and PUT is overwritten — last write wins
82
+ * for the whole representation. The window is one round-trip. Use `replace`
83
+ * to overwrite deliberately.
84
+ *
85
+ * @param id - The todolist ID
86
+ * @param fn - Callback that mutates the todolist's writable fields in place
87
+ * @returns The updated Todolist
88
+ * @throws {BasecampError} If the request fails
89
+ *
90
+ * @example
91
+ * ```ts
92
+ * await client.todolists.edit(123, (t) => {
93
+ * t.name = `🚨 ${t.name}`;
94
+ * t.description = ""; // clearing = setting empty on a full object
95
+ * });
96
+ * ```
97
+ */
98
+ edit(id: number, fn: (t: TodolistFields) => void | Promise<void>): Promise<Todolist>;
99
+ /** Fetches the todolist and derives its full writable state. */
100
+ private currentFields;
101
+ /**
102
+ * PUTs the full writable state via `replace`. Both fields are always sent:
103
+ * description included when empty, because on a full-replace endpoint `""`
104
+ * is how a clear is expressed — never JSON null (SPEC §18 body compaction),
105
+ * and never by omission, which would leave the field to the server's own
106
+ * clear-by-default and read as an accident rather than an intent.
107
+ */
108
+ private putFields;
109
+ }
110
+ //# sourceMappingURL=todolists-extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todolists-extensions.d.ts","sourceRoot":"","sources":["../../src/services/todolists-extensions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACnG,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oCAAoC,CAAC;AAwKnE;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACpC,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,IAAI,EAAE,MAAM,CAAC;IACb,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,qBAAa,gBAAiB,SAAQ,yBAAyB;IAC7D;;;;;;;;;;;;;;;;;;;;;OAqBG;IACG,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,qBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC;IAOvE;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACG,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;IAM1F,gEAAgE;YAClD,aAAa;IAQ3B;;;;;;OAMG;YACW,SAAS;CAyBxB"}
@@ -0,0 +1,262 @@
1
+ import { TodolistsService as GeneratedTodolistsService } from "../generated/services/todolists.js";
2
+ import { Errors, truncateErrorMessage } from "../errors.js";
3
+ /**
4
+ * Renders a value for an error message without ever throwing.
5
+ *
6
+ * The guard's own error path must not fail while explaining a failure.
7
+ * `JSON.stringify` raises `TypeError` on a circular structure, and a value can
8
+ * carry a `toJSON` that throws — either would replace a clean `api_error`/`usage`
9
+ * with an incidental `TypeError` and lose the diagnosis. The type name is always
10
+ * available; the rendering is a bonus, capped per §9 and dropped if it fails.
11
+ */
12
+ function describeValue(value) {
13
+ const kind = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
14
+ try {
15
+ const rendered = JSON.stringify(value);
16
+ return rendered === undefined ? kind : `${kind} ${truncateErrorMessage(rendered)}`;
17
+ }
18
+ catch {
19
+ return kind;
20
+ }
21
+ }
22
+ /**
23
+ * Level 1 of the wire-to-written-value path: the response must be a JSON object
24
+ * before any field is probed.
25
+ *
26
+ * One level up from the malformed-*field* guards. A successful GET can still
27
+ * return a scalar, an array, or null, and each fails a different way without
28
+ * this: `null.name` throws a raw `TypeError` instead of the documented
29
+ * statusless `api_error`, while a scalar or an **array** yields `undefined` for
30
+ * every key and so gets misdiagnosed one rung down as "name is missing from the
31
+ * response" — a body-shape fault reported as a field fault. The array case is
32
+ * the one that used to fall through silently, and `Array.isArray` is what keeps
33
+ * it caught here.
34
+ *
35
+ * Since #544 the route is modelled as one flat `Todolist` — the `TodolistOrGroup`
36
+ * union and its `{ todolist } | { group }` arms are gone, and with them the
37
+ * middle rung of this path. It is object → scalar now: the body (here) and each
38
+ * writable field (`writableString`). A string has no interior, so there is no
39
+ * third. What is emphatically NOT gone is validation: `schema.d.ts` is erased at
40
+ * build time, so a flat shape gives TypeScript no decoder it did not have
41
+ * before. Structural safety for this SDK is #578.
42
+ *
43
+ * The cast is an assertion about the body's shape only. It claims nothing about
44
+ * the fields, which `writableString` still checks one by one.
45
+ */
46
+ function requireTodolistObject(response, operation) {
47
+ if (typeof response !== "object" || response === null || Array.isArray(response)) {
48
+ throw malformedResponse(`${operation} returned ${describeValue(response)} where a todolist object was expected`, "The merge-safe update/edit read this record's fields before rewriting them, so a " +
49
+ "non-object body cannot be used. Use replace() to write the record deliberately.");
50
+ }
51
+ return response;
52
+ }
53
+ /**
54
+ * Validates a caller-supplied writable value, the mirror of the read step.
55
+ *
56
+ * `writableString` owns *response* provenance; this owns *caller* provenance,
57
+ * and the two are one rule seen from opposite ends. `edit` hands the caller a
58
+ * mutable `TodolistFields` and the annotation is erased at build time, so a
59
+ * closure assigning `42` or `[]` — trivially reachable from plain JS, or from
60
+ * TypeScript via `as any` — would otherwise walk straight into the full-replace
61
+ * PUT and write it. That is caller misuse, hence `usage`, where the same wrong
62
+ * type arriving from the server is `api_error`.
63
+ */
64
+ function callerString(fields, key) {
65
+ const value = fields[key];
66
+ if (typeof value !== "string") {
67
+ throw Errors.usage(truncateErrorMessage(`todolist ${key} must be a string, got ${describeValue(value)}`), "The full writable state is PUT back verbatim, so a non-string would be written to the " +
68
+ 'record. Assign a string; use "" to clear.');
69
+ }
70
+ return value;
71
+ }
72
+ /** Builds the malformed-response error, with the message capped per SPEC §9. */
73
+ function malformedResponse(message, hint) {
74
+ // api_error, not usage: the value arrived in a successful API response, so
75
+ // nothing the caller passed is at fault. Statusless — there is no HTTP status
76
+ // to attribute, the transport succeeded — and non-retryable, because
77
+ // re-requesting cannot repair a malformed body.
78
+ return Errors.apiError(truncateErrorMessage(message), undefined, {
79
+ hint,
80
+ retryable: false,
81
+ });
82
+ }
83
+ /**
84
+ * Reads a writable string field off a fetched todolist, refusing to pass a
85
+ * malformed one through.
86
+ *
87
+ * **Classification is by origin, not by value.** The same empty string is a
88
+ * caller error when the caller passed it and malformed response data when it
89
+ * came off the wire, so each provenance is checked where it is unambiguous:
90
+ * this read step owns the response, and `putFields` owns the caller. That is
91
+ * why an empty `name` here is an `api_error` while an empty `name` the caller
92
+ * supplied is a `UsageError` — same value, different origin, different fault.
93
+ *
94
+ * **Presence and non-emptiness are two different claims, and only one of them
95
+ * is per-field.** Since #544 `name` and `description` are both `@required` and
96
+ * never null on this shape — BC3's `format_api_content` funnels a blank rich
97
+ * text through `call_pipeline`, which returns `""` rather than nil — so for
98
+ * *both* fields an absent key and an explicit `null` are malformed, and both
99
+ * are refused here before any PUT. Reading either as `""` would put that `""`
100
+ * in the full-replace body and erase the record's real value on a call that
101
+ * never mentioned the field.
102
+ *
103
+ * `nonEmpty` is the *other* claim and holds for `name` alone: BC3
104
+ * presence-validates the attribute, so no real todolist carries an empty one
105
+ * and `""` off the wire is malformed too. `description` has no such validation
106
+ * — a description-less list carries `""`, which is the ordinary case — so an
107
+ * empty description is a real value, preserved and resent verbatim.
108
+ *
109
+ * Anything of the wrong type is malformed either way: `?? ""` only coalesces
110
+ * null and undefined, so a number, boolean, array or object would ride through
111
+ * **verbatim** into the full-replace PUT and overwrite the real value.
112
+ *
113
+ * The type annotation on the GET result is a compile-time claim about runtime
114
+ * data that nothing validates — `schema.d.ts` is erased at build time, so
115
+ * TypeScript has no decoder that rejects a wrong-typed field the way Go's
116
+ * `json.Unmarshal`, Swift's `Codable`, or kotlinx.serialization do. That puts
117
+ * this composite in the same position as the Python and Ruby ones: the check
118
+ * has to be explicit here. The shipped Todos and Cards analogues are #576.
119
+ */
120
+ function writableString(todolist, key, opts = {}) {
121
+ const value = todolist[key];
122
+ if (!(key in todolist) || value === undefined) {
123
+ throw malformedResponse(`todolist ${key} is missing from the response`, `${key} is required on every todolist, so a body without one is a malformed response, ` +
124
+ "not an empty value to preserve. The merge-safe update/edit PUT the full writable " +
125
+ "state back, so reading it as empty would erase the record's real value.");
126
+ }
127
+ if (value === null) {
128
+ throw malformedResponse(`todolist ${key} is null in the response`, `${key} is required and never null, so a null one is a malformed response, not an empty ` +
129
+ "value to preserve. The merge-safe update/edit PUT the full writable state back, so " +
130
+ "reading it as empty would erase the record's real value.");
131
+ }
132
+ if (typeof value !== "string") {
133
+ throw malformedResponse(`todolist ${key} is not a string: ${describeValue(value)}`, "The merge-safe update/edit resend this field verbatim, so a malformed value would " +
134
+ "overwrite the current one. Use replace() to write the record deliberately.");
135
+ }
136
+ if (opts.nonEmpty && value === "") {
137
+ throw malformedResponse(`todolist ${key} is empty in the response`, `${key} is presence-validated server-side, so an empty one is a malformed response. ` +
138
+ "The caller did not ask to clear it.");
139
+ }
140
+ return value;
141
+ }
142
+ /**
143
+ * TodolistsService with merge-safe `update` and read-modify-write `edit` on
144
+ * top of the generated surface (`get`, `replace`, ...).
145
+ *
146
+ * `PUT /{accountId}/todolists/{id}` is a full replace: BC3's
147
+ * `TodolistsController#update` rebuilds the recordable from only the permitted
148
+ * params, so a sparse PUT that omits `description` erases it, and one that
149
+ * omits `name` is a 422 — the attribute is presence-validated server-side, so
150
+ * omitting it is never a preserve. The writable set is exactly
151
+ * `{name, description}`.
152
+ *
153
+ * The route serves a to-do list and a to-do list group alike, and since #544
154
+ * both are the one flat `Todolist` shape — BC3's
155
+ * `todolists/groups/{index,show}.json.jbuilder` render
156
+ * `todolists/_todolist.json.jbuilder`, so a group reports `"type": "Todolist"`
157
+ * and carries `description`/`description_attachments` like any list. Nothing
158
+ * here branches on the variant; where it matters at all, discrimination is
159
+ * structural (`groups_url` for a Todoset parent, `group_position_url` for a
160
+ * Todolist parent), never the `type` string.
161
+ *
162
+ * Both methods compose the public `get` and `replace` methods, so hooks
163
+ * observe the two wire operations (`GetTodolistOrGroup` then
164
+ * `UpdateTodolistOrGroup`), not a synthetic composite.
165
+ */
166
+ export class TodolistsService extends GeneratedTodolistsService {
167
+ /**
168
+ * Sets the given fields on a todolist and preserves everything else: GETs
169
+ * the current todolist, overlays the explicitly-set request fields, and PUTs
170
+ * the full representation back. An omitted (`undefined`) field is untouched,
171
+ * guaranteed; an explicitly-passed `""` clears the description.
172
+ *
173
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
174
+ * concurrent write between the GET and PUT is overwritten — last write wins
175
+ * for the whole representation. The window is one round-trip. Use `replace`
176
+ * to overwrite deliberately.
177
+ *
178
+ * @param id - The todolist ID
179
+ * @param req - Fields to set; omitted fields are preserved
180
+ * @returns The updated Todolist
181
+ * @throws {BasecampError} If the request fails
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * // Rename without erasing the description.
186
+ * await client.todolists.update(123, { name: "Hardware" });
187
+ * ```
188
+ */
189
+ async update(id, req) {
190
+ const fields = await this.currentFields(id);
191
+ if (req.name !== undefined)
192
+ fields.name = req.name;
193
+ if (req.description !== undefined)
194
+ fields.description = req.description;
195
+ return this.putFields(id, fields);
196
+ }
197
+ /**
198
+ * Applies a read-modify-write callback to a todolist: GETs the current
199
+ * todolist, hands the callback its full writable representation, and PUTs
200
+ * the whole thing back. Clearing the description means setting it empty
201
+ * (`""`) — an untouched field keeps its current value. If the callback
202
+ * throws (or rejects), the edit aborts and nothing is written.
203
+ *
204
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
205
+ * concurrent write between the GET and PUT is overwritten — last write wins
206
+ * for the whole representation. The window is one round-trip. Use `replace`
207
+ * to overwrite deliberately.
208
+ *
209
+ * @param id - The todolist ID
210
+ * @param fn - Callback that mutates the todolist's writable fields in place
211
+ * @returns The updated Todolist
212
+ * @throws {BasecampError} If the request fails
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * await client.todolists.edit(123, (t) => {
217
+ * t.name = `🚨 ${t.name}`;
218
+ * t.description = ""; // clearing = setting empty on a full object
219
+ * });
220
+ * ```
221
+ */
222
+ async edit(id, fn) {
223
+ const fields = await this.currentFields(id);
224
+ await fn(fields);
225
+ return this.putFields(id, fields);
226
+ }
227
+ /** Fetches the todolist and derives its full writable state. */
228
+ async currentFields(id) {
229
+ const current = requireTodolistObject(await this.get(id), "GetTodolistOrGroup");
230
+ return {
231
+ name: writableString(current, "name", { nonEmpty: true }),
232
+ description: writableString(current, "description"),
233
+ };
234
+ }
235
+ /**
236
+ * PUTs the full writable state via `replace`. Both fields are always sent:
237
+ * description included when empty, because on a full-replace endpoint `""`
238
+ * is how a clear is expressed — never JSON null (SPEC §18 body compaction),
239
+ * and never by omission, which would leave the field to the server's own
240
+ * clear-by-default and read as an accident rather than an intent.
241
+ */
242
+ async putFields(id, f) {
243
+ // The caller's side of the origin rule. `currentFields` already rejected an
244
+ // empty name coming off the wire, so by here an empty one can only have been
245
+ // supplied by the caller — genuine misuse, since BC3 presence-validates the
246
+ // attribute and a full write has no state that clears it. Guarding in the
247
+ // composite rather than leaning on the generated `replace()` also aligns
248
+ // this path with the other five SDKs; `replace()` keeps its own
249
+ // `Errors.validation` for direct callers, where the name really is theirs.
250
+ const name = callerString(f, "name");
251
+ const description = callerString(f, "description");
252
+ if (name === "") {
253
+ throw Errors.usage("todolist name must not be empty", "BC3 presence-validates the name, so a full write cannot clear it. Pass a non-empty " +
254
+ "name, or use replace() if you mean to write the record verbatim.");
255
+ }
256
+ // The PUT answers with the written record, which is handed straight back to
257
+ // the caller as a `Todolist`; check its shape too rather than let an array
258
+ // or a scalar out under that type.
259
+ return requireTodolistObject(await this.replace(id, { name, description }), "UpdateTodolistOrGroup");
260
+ }
261
+ }
262
+ //# sourceMappingURL=todolists-extensions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"todolists-extensions.js","sourceRoot":"","sources":["../../src/services/todolists-extensions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AAEnG,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAE5D;;;;;;;;GAQG;AACH,SAAS,aAAa,CAAC,KAAc;IACnC,MAAM,IAAI,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,KAAK,CAAC;IACrF,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,oBAAoB,CAAC,QAAQ,CAAC,EAAE,CAAC;IACrF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,SAAS,qBAAqB,CAAC,QAAiB,EAAE,SAAiB;IACjE,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACjF,MAAM,iBAAiB,CACrB,GAAG,SAAS,aAAa,aAAa,CAAC,QAAQ,CAAC,uCAAuC,EACvF,mFAAmF;YACjF,iFAAiF,CACpF,CAAC;IACJ,CAAC;IACD,OAAO,QAA+B,CAAC;AACzC,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,YAAY,CAAC,MAAsB,EAAE,GAA2B;IACvE,MAAM,KAAK,GAAY,MAAM,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,MAAM,CAAC,KAAK,CAChB,oBAAoB,CAAC,YAAY,GAAG,0BAA0B,aAAa,CAAC,KAAK,CAAC,EAAE,CAAC,EACrF,wFAAwF;YACtF,2CAA2C,CAC9C,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,gFAAgF;AAChF,SAAS,iBAAiB,CAAC,OAAe,EAAE,IAAY;IACtD,2EAA2E;IAC3E,8EAA8E;IAC9E,qEAAqE;IACrE,gDAAgD;IAChD,OAAO,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE;QAC/D,IAAI;QACJ,SAAS,EAAE,KAAK;KACjB,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,SAAS,cAAc,CACrB,QAAkB,EAClB,GAA2B,EAC3B,OAA+B,EAAE;IAEjC,MAAM,KAAK,GAAY,QAAQ,CAAC,GAAG,CAAC,CAAC;IACrC,IAAI,CAAC,CAAC,GAAG,IAAI,QAAQ,CAAC,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QAC9C,MAAM,iBAAiB,CACrB,YAAY,GAAG,+BAA+B,EAC9C,GAAG,GAAG,iFAAiF;YACrF,mFAAmF;YACnF,yEAAyE,CAC5E,CAAC;IACJ,CAAC;IACD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,MAAM,iBAAiB,CACrB,YAAY,GAAG,0BAA0B,EACzC,GAAG,GAAG,mFAAmF;YACvF,qFAAqF;YACrF,0DAA0D,CAC7D,CAAC;IACJ,CAAC;IACD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,iBAAiB,CACrB,YAAY,GAAG,qBAAqB,aAAa,CAAC,KAAK,CAAC,EAAE,EAC1D,oFAAoF;YAClF,4EAA4E,CAC/E,CAAC;IACJ,CAAC;IACD,IAAI,IAAI,CAAC,QAAQ,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;QAClC,MAAM,iBAAiB,CACrB,YAAY,GAAG,2BAA2B,EAC1C,GAAG,GAAG,+EAA+E;YACnF,qCAAqC,CACxC,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AA0BD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,gBAAiB,SAAQ,yBAAyB;IAC7D;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAK,CAAC,MAAM,CAAC,EAAU,EAAE,GAA0B;QACjD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QAC5C,IAAI,GAAG,CAAC,IAAI,KAAK,SAAS;YAAE,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,IAAI,CAAC;QACnD,IAAI,GAAG,CAAC,WAAW,KAAK,SAAS;YAAE,MAAM,CAAC,WAAW,GAAG,GAAG,CAAC,WAAW,CAAC;QACxE,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,KAAK,CAAC,IAAI,CAAC,EAAU,EAAE,EAA+C;QACpE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QAC5C,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;QACjB,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC;IAED,gEAAgE;IACxD,KAAK,CAAC,aAAa,CAAC,EAAU;QACpC,MAAM,OAAO,GAAG,qBAAqB,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,oBAAoB,CAAC,CAAC;QAChF,OAAO;YACL,IAAI,EAAE,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;YACzD,WAAW,EAAE,cAAc,CAAC,OAAO,EAAE,aAAa,CAAC;SACpD,CAAC;IACJ,CAAC;IAED;;;;;;OAMG;IACK,KAAK,CAAC,SAAS,CAAC,EAAU,EAAE,CAAiB;QACnD,4EAA4E;QAC5E,6EAA6E;QAC7E,4EAA4E;QAC5E,0EAA0E;QAC1E,yEAAyE;QACzE,gEAAgE;QAChE,2EAA2E;QAC3E,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;QACrC,MAAM,WAAW,GAAG,YAAY,CAAC,CAAC,EAAE,aAAa,CAAC,CAAC;QACnD,IAAI,IAAI,KAAK,EAAE,EAAE,CAAC;YAChB,MAAM,MAAM,CAAC,KAAK,CAChB,iCAAiC,EACjC,qFAAqF;gBACnF,kEAAkE,CACrE,CAAC;QACJ,CAAC;QACD,4EAA4E;QAC5E,2EAA2E;QAC3E,mCAAmC;QACnC,OAAO,qBAAqB,CAC1B,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,EAC7C,uBAAuB,CACxB,CAAC;IACJ,CAAC;CACF"}