@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,167 @@
1
+ /**
2
+ * Response guards shared by the merge-safe composites.
3
+ *
4
+ * A merge-safe `update`/`edit` GETs a record, reads each writable field, and
5
+ * PUTs the **full** representation back. The endpoint is full-replace, so every
6
+ * value read here is written — including one the caller never mentioned. If the
7
+ * read step coerces or forwards a malformed value instead of refusing it, that
8
+ * value lands on the record.
9
+ *
10
+ * Two failure modes, the same defect wearing different clothes:
11
+ *
12
+ * - **erasure** — a falsey value dropped or coalesced away, wiping the field;
13
+ * - **corruption** — a non-string forwarded verbatim, writing a number,
14
+ * boolean, array or object where a string belongs.
15
+ *
16
+ * `?? ""` catches only `null` and `undefined`, so it rules out erasure while
17
+ * leaving corruption wide open — every one of `false`, `0`, `[]`, `{}`, `42`,
18
+ * `true`, `["x"]` and `{a:1}` rides through unchanged. Testing only for erasure
19
+ * is what let this class survive five review passes, so both are refused here.
20
+ *
21
+ * **The rule: a composite is safe exactly when a decoder *rejects* a
22
+ * wrong-typed field at runtime — not when a type merely claims one.** Go
23
+ * (`json.Unmarshal`), Swift (`Codable`) and Kotlin (kotlinx.serialization)
24
+ * genuinely refuse. TypeScript's `schema.d.ts` is erased at build time, so the
25
+ * type on a GET result is a compile-time claim nothing validates; structurally
26
+ * this sits with Python and Ruby, not with Go and Swift (#576).
27
+ *
28
+ * Todolists carries its own copy of these guards (#574). #544 flattened the
29
+ * shape those guards read — dropping the envelope-arm rung, not the guards —
30
+ * but did not unify them here. A generated validating layer (#578) is the
31
+ * intended end state for all of them.
32
+ */
33
+ import { type BasecampError } from "../errors.js";
34
+ /**
35
+ * Renders a value for an error message without ever throwing.
36
+ *
37
+ * The guard's own error path must not fail while explaining a failure.
38
+ * `JSON.stringify` raises `TypeError` on a circular structure, and a value can
39
+ * carry a `toJSON` that throws — either would replace a clean `api_error` with
40
+ * an incidental `TypeError` and lose the diagnosis. The type name is always
41
+ * available; the rendering is a bonus, capped per SPEC §9 and dropped if it
42
+ * fails.
43
+ */
44
+ export declare function describeValue(value: unknown): string;
45
+ /**
46
+ * Builds the malformed-response error, with the message capped per SPEC §9.
47
+ *
48
+ * `api_error`, not `usage`: the value arrived in a successful API response, so
49
+ * nothing the caller passed is at fault. Statusless — the transport succeeded,
50
+ * there is no HTTP status to attribute — and non-retryable, because
51
+ * re-requesting cannot repair a malformed body.
52
+ */
53
+ export declare function malformedResponse(message: string, hint: string): BasecampError;
54
+ /**
55
+ * The response must be a JSON object before any field is read.
56
+ *
57
+ * One level up from the malformed-*field* guards: a successful GET can return a
58
+ * scalar, an array, or null. Reading a property off `null` throws a raw
59
+ * `TypeError`, and off an array or a string it silently yields `undefined`,
60
+ * which the field guards would then read as "genuinely empty" and write back —
61
+ * so the envelope needs checking before the fields.
62
+ */
63
+ export declare function requireRecord(body: unknown, opts: {
64
+ record: string;
65
+ operation: string;
66
+ escape: string;
67
+ }): Record<string, unknown>;
68
+ /**
69
+ * Reads a writable string field, refusing to pass a malformed one through.
70
+ *
71
+ * An absent key or an explicit `null` is genuinely empty — there is nothing to
72
+ * preserve and `""` is what the server already holds. An actual string passes
73
+ * verbatim. Anything else is a malformed response and is refused **before** the
74
+ * PUT, naming the offending field.
75
+ */
76
+ export declare function writableString(body: Record<string, unknown>, key: string, opts: {
77
+ record: string;
78
+ escape: string;
79
+ }): string;
80
+ /**
81
+ * Reads a writable string the record is *required* to carry.
82
+ *
83
+ * {@link writableString} treats an absent key or an explicit `null` as
84
+ * genuinely empty, which is right for an optional field — `""` is what the
85
+ * server already holds. It is wrong for a required one. Where the spec marks a
86
+ * response member `@required` and BC3 can never render it blank, an absent,
87
+ * null or blank value in a 2xx body is a **malformed response**, not an empty
88
+ * field. Coalescing it to `""` and sending that in the full-replace PUT would
89
+ * blank the real value on a call that never mentioned it — #576's defect
90
+ * exactly.
91
+ *
92
+ * Two records rely on this today and for the same reason: `Document#title` is
93
+ * `super.presence || "Untitled"` and `Schedule::Entry#summary` is
94
+ * `super.presence || "Untitled"`, so neither can come back blank from a healthy
95
+ * server.
96
+ *
97
+ * The wrong-type branch is delegated to {@link writableString}, so a required
98
+ * field and an optional one report a non-string identically.
99
+ */
100
+ export declare function requiredWritableString(body: Record<string, unknown>, key: string, opts: {
101
+ record: string;
102
+ escape: string;
103
+ }): string;
104
+ /**
105
+ * Reads a writable boolean the record is *required* to carry.
106
+ *
107
+ * The boolean analogue of {@link requiredWritableString}, and it cannot be
108
+ * expressed with a truthiness test: the value this guard most needs to admit is
109
+ * `false`, which every `||`/`if (!x)` idiom would treat as missing and replace
110
+ * with a default. `ScheduleEntry.all_day` is `NOT NULL` with a `false` default
111
+ * in BC3 and every partial emits it, so absent or null is a malformed
112
+ * response — and defaulting it to `false` would silently convert an all-day
113
+ * event into a midnight-to-midnight timed one on a call that only changed the
114
+ * summary.
115
+ *
116
+ * `0`/`1` are refused rather than coerced, for the same reason
117
+ * {@link writableString} refuses `42`: JSON has a boolean type and the server
118
+ * uses it.
119
+ */
120
+ export declare function requiredWritableBoolean(body: Record<string, unknown>, key: string, opts: {
121
+ record: string;
122
+ escape: string;
123
+ }): boolean;
124
+ /**
125
+ * Reads an *optional* writable boolean, refusing to coerce a malformed one.
126
+ *
127
+ * {@link writableString}'s boolean sibling, standing in the same relation to
128
+ * {@link requiredWritableBoolean} that `writableString` does to
129
+ * {@link requiredWritableString}: an absent key or an explicit `null` is
130
+ * genuinely "not set" and returns `false`, because that is what the server
131
+ * already holds.
132
+ *
133
+ * `ScheduleEntry.highlighted` is the case it exists for. The entry partial
134
+ * emits it unconditionally, but the reduced calendar partial behind
135
+ * `GetUpcomingSchedule` does not, and both render through the same schema — so
136
+ * the member is optional and absence is legitimate rather than malformed.
137
+ *
138
+ * What still cannot be tolerated is the *wrong type*: a `"yes"` or a `1` must be
139
+ * refused, not coerced, because a caller who assigns the seeded value straight
140
+ * back sends whatever it was seeded with. That branch is delegated to
141
+ * {@link requiredWritableBoolean}, so an optional boolean and a required one
142
+ * report a non-boolean identically.
143
+ */
144
+ export declare function writableBoolean(body: Record<string, unknown>, key: string, opts: {
145
+ record: string;
146
+ escape: string;
147
+ }): boolean;
148
+ /**
149
+ * Reads a list of person records and projects it to their integer IDs.
150
+ *
151
+ * The analogue of {@link writableString} for the ID-list fields. The `.map()`
152
+ * it replaces (`(body[key] ?? []).map((p) => p.id)`) has three ways to go wrong
153
+ * on malformed data: a non-array has no `.map` (a raw `TypeError`), a
154
+ * non-object element yields `undefined`, and a non-integer `id` rides through
155
+ * verbatim into the full-replace PUT — the same corruption as a wrong-typed
156
+ * string, one level down.
157
+ *
158
+ * `Number.isInteger` is the test rather than `typeof === "number"`: `1.5` and
159
+ * `NaN` are numbers and neither is a person ID. Booleans fail it outright,
160
+ * which is what we want — JavaScript would happily coerce `true` to `1`
161
+ * downstream.
162
+ */
163
+ export declare function writableIdList(body: Record<string, unknown>, key: string, opts: {
164
+ record: string;
165
+ escape: string;
166
+ }): number[];
167
+ //# sourceMappingURL=merge-safe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-safe.d.ts","sourceRoot":"","sources":["../../src/services/merge-safe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,OAAO,EAAgC,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAMhF;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQpD;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,aAAa,CAE9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAC3B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC1D,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CASzB;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC,MAAM,CAUR;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC,MAAM,CAUR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC,OAAO,CAgBT;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC,OAAO,CAIT;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GACvC,MAAM,EAAE,CA+BV"}
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Response guards shared by the merge-safe composites.
3
+ *
4
+ * A merge-safe `update`/`edit` GETs a record, reads each writable field, and
5
+ * PUTs the **full** representation back. The endpoint is full-replace, so every
6
+ * value read here is written — including one the caller never mentioned. If the
7
+ * read step coerces or forwards a malformed value instead of refusing it, that
8
+ * value lands on the record.
9
+ *
10
+ * Two failure modes, the same defect wearing different clothes:
11
+ *
12
+ * - **erasure** — a falsey value dropped or coalesced away, wiping the field;
13
+ * - **corruption** — a non-string forwarded verbatim, writing a number,
14
+ * boolean, array or object where a string belongs.
15
+ *
16
+ * `?? ""` catches only `null` and `undefined`, so it rules out erasure while
17
+ * leaving corruption wide open — every one of `false`, `0`, `[]`, `{}`, `42`,
18
+ * `true`, `["x"]` and `{a:1}` rides through unchanged. Testing only for erasure
19
+ * is what let this class survive five review passes, so both are refused here.
20
+ *
21
+ * **The rule: a composite is safe exactly when a decoder *rejects* a
22
+ * wrong-typed field at runtime — not when a type merely claims one.** Go
23
+ * (`json.Unmarshal`), Swift (`Codable`) and Kotlin (kotlinx.serialization)
24
+ * genuinely refuse. TypeScript's `schema.d.ts` is erased at build time, so the
25
+ * type on a GET result is a compile-time claim nothing validates; structurally
26
+ * this sits with Python and Ruby, not with Go and Swift (#576).
27
+ *
28
+ * Todolists carries its own copy of these guards (#574). #544 flattened the
29
+ * shape those guards read — dropping the envelope-arm rung, not the guards —
30
+ * but did not unify them here. A generated validating layer (#578) is the
31
+ * intended end state for all of them.
32
+ */
33
+ import { Errors, truncateErrorMessage } from "../errors.js";
34
+ const resendHint = (escape) => "The merge-safe update/edit resend this field verbatim, so a coerced or empty value " +
35
+ `would overwrite the current one. Use ${escape} to write the record deliberately.`;
36
+ /**
37
+ * Renders a value for an error message without ever throwing.
38
+ *
39
+ * The guard's own error path must not fail while explaining a failure.
40
+ * `JSON.stringify` raises `TypeError` on a circular structure, and a value can
41
+ * carry a `toJSON` that throws — either would replace a clean `api_error` with
42
+ * an incidental `TypeError` and lose the diagnosis. The type name is always
43
+ * available; the rendering is a bonus, capped per SPEC §9 and dropped if it
44
+ * fails.
45
+ */
46
+ export function describeValue(value) {
47
+ const kind = value === null ? "null" : Array.isArray(value) ? "array" : typeof value;
48
+ try {
49
+ const rendered = JSON.stringify(value);
50
+ return rendered === undefined ? kind : `${kind} ${truncateErrorMessage(rendered)}`;
51
+ }
52
+ catch {
53
+ return kind;
54
+ }
55
+ }
56
+ /**
57
+ * Builds the malformed-response error, with the message capped per SPEC §9.
58
+ *
59
+ * `api_error`, not `usage`: the value arrived in a successful API response, so
60
+ * nothing the caller passed is at fault. Statusless — the transport succeeded,
61
+ * there is no HTTP status to attribute — and non-retryable, because
62
+ * re-requesting cannot repair a malformed body.
63
+ */
64
+ export function malformedResponse(message, hint) {
65
+ return Errors.apiError(truncateErrorMessage(message), undefined, { hint, retryable: false });
66
+ }
67
+ /**
68
+ * The response must be a JSON object before any field is read.
69
+ *
70
+ * One level up from the malformed-*field* guards: a successful GET can return a
71
+ * scalar, an array, or null. Reading a property off `null` throws a raw
72
+ * `TypeError`, and off an array or a string it silently yields `undefined`,
73
+ * which the field guards would then read as "genuinely empty" and write back —
74
+ * so the envelope needs checking before the fields.
75
+ */
76
+ export function requireRecord(body, opts) {
77
+ if (typeof body !== "object" || body === null || Array.isArray(body)) {
78
+ throw malformedResponse(`${opts.operation} returned ${describeValue(body)} where a ${opts.record.toLowerCase()} object was expected`, "The merge-safe update/edit read this record's fields before rewriting them, so a " +
79
+ `non-object body cannot be used. Use ${opts.escape} to write the record deliberately.`);
80
+ }
81
+ return body;
82
+ }
83
+ /**
84
+ * Reads a writable string field, refusing to pass a malformed one through.
85
+ *
86
+ * An absent key or an explicit `null` is genuinely empty — there is nothing to
87
+ * preserve and `""` is what the server already holds. An actual string passes
88
+ * verbatim. Anything else is a malformed response and is refused **before** the
89
+ * PUT, naming the offending field.
90
+ */
91
+ export function writableString(body, key, opts) {
92
+ const value = body[key];
93
+ if (value === undefined || value === null)
94
+ return "";
95
+ if (typeof value !== "string") {
96
+ throw malformedResponse(`${opts.record} field "${key}" is not a string: ${describeValue(value)}`, resendHint(opts.escape));
97
+ }
98
+ return value;
99
+ }
100
+ /**
101
+ * Reads a writable string the record is *required* to carry.
102
+ *
103
+ * {@link writableString} treats an absent key or an explicit `null` as
104
+ * genuinely empty, which is right for an optional field — `""` is what the
105
+ * server already holds. It is wrong for a required one. Where the spec marks a
106
+ * response member `@required` and BC3 can never render it blank, an absent,
107
+ * null or blank value in a 2xx body is a **malformed response**, not an empty
108
+ * field. Coalescing it to `""` and sending that in the full-replace PUT would
109
+ * blank the real value on a call that never mentioned it — #576's defect
110
+ * exactly.
111
+ *
112
+ * Two records rely on this today and for the same reason: `Document#title` is
113
+ * `super.presence || "Untitled"` and `Schedule::Entry#summary` is
114
+ * `super.presence || "Untitled"`, so neither can come back blank from a healthy
115
+ * server.
116
+ *
117
+ * The wrong-type branch is delegated to {@link writableString}, so a required
118
+ * field and an optional one report a non-string identically.
119
+ */
120
+ export function requiredWritableString(body, key, opts) {
121
+ const value = body[key];
122
+ if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
123
+ throw malformedResponse(`${opts.record} field "${key}" is required but the response carried ${describeValue(value)}`, `The merge-safe update/edit resend this field verbatim, so a missing or blank value would ` +
124
+ `blank the current one. Use ${opts.escape} to write the record deliberately.`);
125
+ }
126
+ return writableString(body, key, opts);
127
+ }
128
+ /**
129
+ * Reads a writable boolean the record is *required* to carry.
130
+ *
131
+ * The boolean analogue of {@link requiredWritableString}, and it cannot be
132
+ * expressed with a truthiness test: the value this guard most needs to admit is
133
+ * `false`, which every `||`/`if (!x)` idiom would treat as missing and replace
134
+ * with a default. `ScheduleEntry.all_day` is `NOT NULL` with a `false` default
135
+ * in BC3 and every partial emits it, so absent or null is a malformed
136
+ * response — and defaulting it to `false` would silently convert an all-day
137
+ * event into a midnight-to-midnight timed one on a call that only changed the
138
+ * summary.
139
+ *
140
+ * `0`/`1` are refused rather than coerced, for the same reason
141
+ * {@link writableString} refuses `42`: JSON has a boolean type and the server
142
+ * uses it.
143
+ */
144
+ export function requiredWritableBoolean(body, key, opts) {
145
+ const value = body[key];
146
+ if (value === undefined || value === null) {
147
+ throw malformedResponse(`${opts.record} field "${key}" is required but the response carried ${describeValue(value)}`, `The merge-safe update/edit resend this field verbatim, so a missing value would replace ` +
148
+ `the current one with a default. Use ${opts.escape} to write the record deliberately.`);
149
+ }
150
+ if (typeof value !== "boolean") {
151
+ throw malformedResponse(`${opts.record} field "${key}" is not a boolean: ${describeValue(value)}`, resendHint(opts.escape));
152
+ }
153
+ return value;
154
+ }
155
+ /**
156
+ * Reads an *optional* writable boolean, refusing to coerce a malformed one.
157
+ *
158
+ * {@link writableString}'s boolean sibling, standing in the same relation to
159
+ * {@link requiredWritableBoolean} that `writableString` does to
160
+ * {@link requiredWritableString}: an absent key or an explicit `null` is
161
+ * genuinely "not set" and returns `false`, because that is what the server
162
+ * already holds.
163
+ *
164
+ * `ScheduleEntry.highlighted` is the case it exists for. The entry partial
165
+ * emits it unconditionally, but the reduced calendar partial behind
166
+ * `GetUpcomingSchedule` does not, and both render through the same schema — so
167
+ * the member is optional and absence is legitimate rather than malformed.
168
+ *
169
+ * What still cannot be tolerated is the *wrong type*: a `"yes"` or a `1` must be
170
+ * refused, not coerced, because a caller who assigns the seeded value straight
171
+ * back sends whatever it was seeded with. That branch is delegated to
172
+ * {@link requiredWritableBoolean}, so an optional boolean and a required one
173
+ * report a non-boolean identically.
174
+ */
175
+ export function writableBoolean(body, key, opts) {
176
+ const value = body[key];
177
+ if (value === undefined || value === null)
178
+ return false;
179
+ return requiredWritableBoolean(body, key, opts);
180
+ }
181
+ /**
182
+ * Reads a list of person records and projects it to their integer IDs.
183
+ *
184
+ * The analogue of {@link writableString} for the ID-list fields. The `.map()`
185
+ * it replaces (`(body[key] ?? []).map((p) => p.id)`) has three ways to go wrong
186
+ * on malformed data: a non-array has no `.map` (a raw `TypeError`), a
187
+ * non-object element yields `undefined`, and a non-integer `id` rides through
188
+ * verbatim into the full-replace PUT — the same corruption as a wrong-typed
189
+ * string, one level down.
190
+ *
191
+ * `Number.isInteger` is the test rather than `typeof === "number"`: `1.5` and
192
+ * `NaN` are numbers and neither is a person ID. Booleans fail it outright,
193
+ * which is what we want — JavaScript would happily coerce `true` to `1`
194
+ * downstream.
195
+ */
196
+ export function writableIdList(body, key, opts) {
197
+ const value = body[key];
198
+ if (value === undefined || value === null)
199
+ return [];
200
+ if (!Array.isArray(value)) {
201
+ throw malformedResponse(`${opts.record} field "${key}" is not an array: ${describeValue(value)}`, resendHint(opts.escape));
202
+ }
203
+ return value.map((element, index) => {
204
+ if (typeof element !== "object" || element === null || Array.isArray(element)) {
205
+ throw malformedResponse(`${opts.record} field "${key}"[${index}] is not an object: ${describeValue(element)}`, resendHint(opts.escape));
206
+ }
207
+ const id = element["id"];
208
+ if (id === undefined || id === null) {
209
+ throw malformedResponse(`${opts.record} field "${key}"[${index}] has no "id"`, resendHint(opts.escape));
210
+ }
211
+ if (typeof id !== "number" || !Number.isInteger(id)) {
212
+ throw malformedResponse(`${opts.record} field "${key}"[${index}].id is not an integer: ${describeValue(id)}`, resendHint(opts.escape));
213
+ }
214
+ return id;
215
+ });
216
+ }
217
+ //# sourceMappingURL=merge-safe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"merge-safe.js","sourceRoot":"","sources":["../../src/services/merge-safe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,266 @@
1
+ import { SchedulesService as GeneratedSchedulesService } from "../generated/services/schedules.js";
2
+ import type { ScheduleEntry } from "../generated/services/schedules.js";
3
+ /**
4
+ * Request parameters for `updateEntry`.
5
+ *
6
+ * **Addressedness is property presence, not value.** Every field is optional;
7
+ * a key you leave off the object is not addressed, and a key you set is —
8
+ * including `{ highlighted: false }`, `{ url: "" }` and
9
+ * `{ participantIds: [] }`, each of which is a deliberate clear and reaches the
10
+ * wire. That distinction is the whole point of the carve-out contract below,
11
+ * so it is tested with `Object.prototype.hasOwnProperty.call`, never with a
12
+ * truthiness test.
13
+ *
14
+ * The fields split in two, and the split is not cosmetic:
15
+ *
16
+ * - **full state** — `summary`, `startsAt`, `endsAt`, `description`, `allDay`.
17
+ * Always sent, seeded from the read-back when you do not address them,
18
+ * because BC3 rebuilds the entry from the submitted params and clears every
19
+ * writable field the body omits.
20
+ * - **addressed-only** — `participantIds`, `url`, `highlighted`, `notify`.
21
+ * Sent only when you address them, and never seeded from the read-back. BC3
22
+ * preserves the first three on omission (`PRESERVED_ON_OMISSION`), so
23
+ * resending them is redundant at best and wrong if the read raced a
24
+ * concurrent change. `notify` is a directive rather than state: sending it
25
+ * makes BC3 recompute a drafted entry's subscriber list.
26
+ */
27
+ export interface UpdateScheduleEntryRequest {
28
+ /** Summary text. Omit to leave unchanged; `""` clears it. */
29
+ summary?: string;
30
+ /**
31
+ * When the entry starts — a bare date (`"2026-06-05"`) for an all-day entry,
32
+ * a timestamp otherwise. Omit to leave unchanged.
33
+ */
34
+ startsAt?: string;
35
+ /** When the entry ends, in the same spelling as `startsAt`. Omit to leave unchanged. */
36
+ endsAt?: string;
37
+ /** Rich text description (HTML). Omit to leave unchanged; `""` clears it. */
38
+ description?: string;
39
+ /** Whether the entry occupies whole days. Omit to leave unchanged. */
40
+ allDay?: boolean;
41
+ /**
42
+ * Replaces the entry's participants. Omit to leave them alone — BC3 preserves
43
+ * them when the request does not address them — or pass `[]` to remove
44
+ * everyone.
45
+ *
46
+ * Never resent on your behalf: the read-back's `participants` are people the
47
+ * *reader* can see, and echoing them would rewrite the list from a stale view.
48
+ */
49
+ participantIds?: number[];
50
+ /**
51
+ * The entry's join link — a video-call URL or similar. Omit to leave it
52
+ * alone; `""` clears it.
53
+ *
54
+ * Read back as `join_url`, never as `url`: an entry's `url` is its own
55
+ * Basecamp API URL. The composite therefore never seeds this from the
56
+ * response, which would write the API URL into the join link.
57
+ */
58
+ url?: string;
59
+ /** Whether the entry is highlighted on the schedule. Omit to leave it alone; `false` removes it. */
60
+ highlighted?: boolean;
61
+ /**
62
+ * Whether to notify the relevant people. A directive, not state: it is sent
63
+ * only when you address it, and it is never read back.
64
+ */
65
+ notify?: boolean;
66
+ }
67
+ /**
68
+ * A schedule entry's full writable state, handed to the `editEntry` callback.
69
+ *
70
+ * The five full-state members are seeded from the read-back and **always** PUT
71
+ * back, so clearing one means setting it empty (`""`) — there is no third
72
+ * state.
73
+ *
74
+ * The four carve-outs behave differently and deliberately: they are seeded for
75
+ * *reading* (so you can inspect the current join link, highlight and
76
+ * participants before deciding), but they reach the wire **only if you assign
77
+ * to them**. Assignment is what marks them, not the value you assign — setting
78
+ * `e.url = e.url` sends the join link, because intent is not recoverable from a
79
+ * value comparison.
80
+ */
81
+ export interface ScheduleEntryFields {
82
+ /** Summary text. Set `""` to clear — the entry then reads back as "Untitled". */
83
+ summary: string;
84
+ /** Start date or timestamp, exactly as the server spelled it. */
85
+ startsAt: string;
86
+ /** End date or timestamp, exactly as the server spelled it. */
87
+ endsAt: string;
88
+ /** Rich text description (HTML). Set `""` to clear. */
89
+ description: string;
90
+ /** Whether the entry occupies whole days. */
91
+ allDay: boolean;
92
+ /** Current participants, by person ID. Assign to replace them; `[]` removes everyone. */
93
+ participantIds: number[];
94
+ /** The current join link (the response's `join_url`), or `""`. Assign to change it. */
95
+ url: string;
96
+ /** Whether the entry is currently highlighted. Assign to change it. */
97
+ highlighted: boolean;
98
+ /**
99
+ * Whether to notify the relevant people. Write-only — the API reports no such
100
+ * field, so this reads back `undefined` until you assign to it.
101
+ */
102
+ notify?: boolean;
103
+ }
104
+ /**
105
+ * SchedulesService with merge-safe `updateEntry` and read-modify-write
106
+ * `editEntry` on top of the generated surface (`getEntry`, `replaceEntry`,
107
+ * `createEntry`, `listEntries`, ...).
108
+ *
109
+ * `PUT /{accountId}/schedule_entries/{entryId}` is a full replace:
110
+ * `Schedules::EntriesController#update` rebuilds the recordable from the
111
+ * submitted params, so a sparse PUT that omits `description` erases it and one
112
+ * that omits `all_day` turns an all-day event into a midnight-to-midnight timed
113
+ * one. Neither is a 422 — both are a `200` that quietly clears, which is what
114
+ * these composites exist to prevent.
115
+ *
116
+ * Three writable fields are exempt. BC3 seeds `participant_ids`, `url` and
117
+ * `highlighted` from the existing recordable when the request does not address
118
+ * them, so the SDK's job for those is the opposite one: keep them **off** the
119
+ * wire unless the caller asked for them.
120
+ *
121
+ * Both composites compose the public `getEntry` and `replaceEntry`, so hooks
122
+ * observe the two wire operations under their native identities
123
+ * (`GetScheduleEntry` then `ReplaceScheduleEntry`) rather than a synthetic
124
+ * composite.
125
+ *
126
+ * **Non-recurring entries only.** `ensure_non_recurring_event` 302-redirects
127
+ * both `show` and `update` for a recurring entry, and the SDK follows neither:
128
+ * the GET surfaces as an unexpected body and the PUT as a redirect. Recurrence
129
+ * itself (`recurrence_schedule`, `recurs_until`, `time_zone_name`) is unmodeled
130
+ * — BC3 forces all three to nil for a non-recurring entry.
131
+ */
132
+ export declare class SchedulesService extends GeneratedSchedulesService {
133
+ /**
134
+ * Sets the given fields on a schedule entry and preserves everything else:
135
+ * GETs the current entry, overlays the fields you addressed, and PUTs the
136
+ * full representation back.
137
+ *
138
+ * A key you leave off the request is not addressed. For the five full-state
139
+ * fields that means "keep what the server has"; for the four carve-outs it
140
+ * means the key never appears in the body, so BC3's own preservation applies.
141
+ * An explicitly-passed `""`, `false` or `[]` is an address and clears.
142
+ *
143
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
144
+ * concurrent write between the GET and PUT is overwritten — last write wins
145
+ * for the full-state fields. The window is one round-trip. Use `replaceEntry`
146
+ * to overwrite deliberately.
147
+ *
148
+ * @param entryId - The entry ID
149
+ * @param req - Fields to set; unaddressed fields are preserved
150
+ * @returns The updated ScheduleEntry
151
+ * @throws {BasecampError} If the request fails, or if the read-back is malformed
152
+ *
153
+ * @example
154
+ * ```ts
155
+ * // Rename without erasing the description, times or all-day flag.
156
+ * await client.schedules.updateEntry(123, { summary: "Team Meeting & Kickoff" });
157
+ *
158
+ * // Drop the join link and the highlight; participants are left alone.
159
+ * await client.schedules.updateEntry(123, { url: "", highlighted: false });
160
+ * ```
161
+ */
162
+ updateEntry(entryId: number, req: UpdateScheduleEntryRequest): Promise<ScheduleEntry>;
163
+ /**
164
+ * Applies a read-modify-write callback to a schedule entry: GETs the current
165
+ * entry, hands the callback its writable state, and PUTs the whole thing
166
+ * back. If the callback throws (or rejects), the edit aborts and nothing is
167
+ * written.
168
+ *
169
+ * The five full-state fields always ride back, so clearing one means setting
170
+ * it empty and an untouched one keeps its current value. The four carve-outs
171
+ * (`participantIds`, `url`, `highlighted`, `notify`) are seeded so you can
172
+ * read them, but they reach the wire only if the callback **assigns** to
173
+ * them. Assignment is tracked by invocation, not by comparing values:
174
+ * `e.url = e.url` is a write, because a caller who reassigns the value the
175
+ * read returned still means "write this", and no diff can recover that
176
+ * intent.
177
+ *
178
+ * Not atomic: there is no conditional-update signal on this endpoint, so a
179
+ * concurrent write between the GET and PUT is overwritten — last write wins
180
+ * for the full-state fields. The window is one round-trip. Use `replaceEntry`
181
+ * to overwrite deliberately.
182
+ *
183
+ * @param entryId - The entry ID
184
+ * @param fn - Callback that mutates the entry's writable fields in place
185
+ * @returns The updated ScheduleEntry
186
+ * @throws {BasecampError} If the request fails, or if the read-back is malformed
187
+ *
188
+ * @example
189
+ * ```ts
190
+ * await client.schedules.editEntry(123, (e) => {
191
+ * e.summary = `🚨 ${e.summary}`;
192
+ * e.description = ""; // clearing = setting empty on a full object
193
+ * // participants, join link and highlight are untouched, so they stay off
194
+ * // the wire and BC3 keeps them.
195
+ * });
196
+ * ```
197
+ */
198
+ editEntry(entryId: number, fn: (e: ScheduleEntryFields) => void | Promise<void>): Promise<ScheduleEntry>;
199
+ /**
200
+ * Fetches the entry and derives the full-state half of its writable state.
201
+ *
202
+ * Every value here is resent in the full-replace PUT, so every value is
203
+ * validated before it is read. `?? ""` coalesces only `null` and `undefined`,
204
+ * leaving corruption wide open: `false`, `0`, `[]`, `{}`, `42`, `true` and
205
+ * friends would all be forwarded **verbatim** and written to the entry on a
206
+ * call that never mentioned the field. Nothing below this rejects them —
207
+ * `schema.d.ts` is erased at build time, so `ScheduleEntry` is a compile-time
208
+ * claim about runtime data, which puts this composite with Python and Ruby
209
+ * rather than with Go and Swift. See `merge-safe.ts` and #576.
210
+ *
211
+ * The guards differ per field because the spec models the fields
212
+ * differently:
213
+ *
214
+ * - `summary` is `@required` and `Schedule::Entry#summary` is
215
+ * `super.presence || "Untitled"`, so blank is impossible from a healthy
216
+ * server and absent/null/blank is malformed.
217
+ * - `starts_at`/`ends_at` are `@required` and `NOT NULL`. The value is a bare
218
+ * date for an all-day entry and a timestamp otherwise; it round-trips
219
+ * verbatim rather than through a `Date`, which would normalise the two
220
+ * spellings into one and rewrite the entry's shape.
221
+ * - `all_day` is `@required`, `NOT NULL DEFAULT false`. It needs the boolean
222
+ * guard specifically because a truthiness test would read the legitimate
223
+ * `false` as missing — and defaulting a missing value to `false` converts
224
+ * an all-day event into a midnight-to-midnight timed one.
225
+ * - `description` is optional and nullable both ways, so absent or null is a
226
+ * genuinely empty description.
227
+ */
228
+ private currentState;
229
+ /**
230
+ * The full-state fields plus the carve-out seeds the `editEntry` callback can
231
+ * read.
232
+ *
233
+ * `url` is seeded from the response's **`join_url`**, not its `url`: the
234
+ * latter is the entry's own Basecamp API URL, written by
235
+ * `recordings/_recording` before the entry partial renders. Echoing it into
236
+ * the request's `url` would write the API URL into the join link.
237
+ *
238
+ * `updateEntry` deliberately does not come through here. It never seeds a
239
+ * carve-out, so validating `participants` for it would refuse a write that
240
+ * does not depend on them.
241
+ */
242
+ private currentFields;
243
+ /**
244
+ * GETs the entry and checks the envelope before any field is read.
245
+ *
246
+ * A successful GET can still return a scalar, an array or null; reading a
247
+ * property off `null` throws a raw `TypeError` instead of the documented
248
+ * statusless `api_error`, and reading one off an array or string silently
249
+ * yields `undefined`, which the field guards would then take at face value.
250
+ */
251
+ private readEntry;
252
+ /**
253
+ * PUTs the full writable state via `replaceEntry`, plus whichever carve-outs
254
+ * the caller addressed.
255
+ *
256
+ * The five full-state fields are always named, empties included: on a
257
+ * full-replace endpoint `""` is how a clear is expressed — never JSON null
258
+ * (SPEC §18 body compaction), and never by omission, which would leave the
259
+ * field to BC3's clear-by-default and read as an accident rather than an
260
+ * intent. The carve-outs are the mirror image: an unaddressed one is absent
261
+ * from `carveOuts`, so the generated request builder leaves it `undefined`
262
+ * and it never reaches the wire.
263
+ */
264
+ private putEntry;
265
+ }
266
+ //# sourceMappingURL=schedules-extensions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedules-extensions.d.ts","sourceRoot":"","sources":["../../src/services/schedules-extensions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,IAAI,yBAAyB,EAAE,MAAM,oCAAoC,CAAC;AACnG,OAAO,KAAK,EAEV,aAAa,EACd,MAAM,oCAAoC,CAAC;AAuB5C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,WAAW,0BAA0B;IACzC,6DAA6D;IAC7D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;;;;;OAOG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oGAAoG;IACpG,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,mBAAmB;IAClC,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,6CAA6C;IAC7C,MAAM,EAAE,OAAO,CAAC;IAChB,yFAAyF;IACzF,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,uFAAuF;IACvF,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,WAAW,EAAE,OAAO,CAAC;IACrB;;;OAGG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AASD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,qBAAa,gBAAiB,SAAQ,yBAAyB;IAC7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;IACG,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,0BAA0B,GAAG,OAAO,CAAC,aAAa,CAAC;IAgC3F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACG,SAAS,CACb,OAAO,EAAE,MAAM,EACf,EAAE,EAAE,CAAC,CAAC,EAAE,mBAAmB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACnD,OAAO,CAAC,aAAa,CAAC;IA0BzB;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA4BG;YACW,YAAY;IAW1B;;;;;;;;;;;;OAYG;YACW,aAAa;IAc3B;;;;;;;OAOG;YACW,SAAS;IAQvB;;;;;;;;;;;OAWG;IACH,OAAO,CAAC,QAAQ;CAcjB"}