@remit/calendar-service 0.0.17 → 0.0.19

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 (73) hide show
  1. package/dist/accept.d.ts +71 -0
  2. package/dist/accept.js +159 -0
  3. package/dist/build.d.ts +79 -0
  4. package/dist/build.js +196 -0
  5. package/dist/correct.d.ts +4 -0
  6. package/dist/correct.js +22 -0
  7. package/dist/errors.d.ts +56 -0
  8. package/dist/errors.js +1 -0
  9. package/dist/etag.d.ts +9 -0
  10. package/{src/etag.ts → dist/etag.js} +1 -3
  11. package/dist/expand.d.ts +73 -0
  12. package/dist/expand.js +188 -0
  13. package/dist/feed.d.ts +81 -0
  14. package/dist/feed.js +157 -0
  15. package/dist/fixtures.d.ts +19 -0
  16. package/dist/fixtures.js +37 -0
  17. package/dist/index.d.ts +16 -0
  18. package/dist/index.js +15 -0
  19. package/dist/memory-store.d.ts +105 -0
  20. package/dist/memory-store.js +319 -0
  21. package/dist/parse.d.ts +39 -0
  22. package/dist/parse.js +87 -0
  23. package/dist/project.d.ts +30 -0
  24. package/dist/project.js +57 -0
  25. package/dist/put.d.ts +52 -0
  26. package/dist/put.js +78 -0
  27. package/dist/reopen.d.ts +14 -0
  28. package/dist/reopen.js +92 -0
  29. package/dist/scope.d.ts +70 -0
  30. package/dist/scope.js +687 -0
  31. package/dist/subscribe.d.ts +96 -0
  32. package/dist/subscribe.js +298 -0
  33. package/dist/suggest.d.ts +74 -0
  34. package/dist/suggest.js +138 -0
  35. package/dist/time.d.ts +76 -0
  36. package/dist/time.js +164 -0
  37. package/dist/window.d.ts +85 -0
  38. package/dist/window.js +161 -0
  39. package/package.json +40 -35
  40. package/src/accept.test.ts +0 -403
  41. package/src/accept.ts +0 -229
  42. package/src/build.test.ts +0 -113
  43. package/src/build.ts +0 -311
  44. package/src/correct.test.ts +0 -173
  45. package/src/correct.ts +0 -40
  46. package/src/errors.ts +0 -62
  47. package/src/etag.test.ts +0 -54
  48. package/src/expand.test.ts +0 -361
  49. package/src/expand.ts +0 -303
  50. package/src/feed.test.ts +0 -334
  51. package/src/feed.ts +0 -203
  52. package/src/feeds/google-secret-address.ics +0 -99
  53. package/src/fixtures.ts +0 -52
  54. package/src/index.ts +0 -125
  55. package/src/memory-store.ts +0 -404
  56. package/src/parse.test.ts +0 -264
  57. package/src/parse.ts +0 -138
  58. package/src/project.test.ts +0 -247
  59. package/src/project.ts +0 -122
  60. package/src/put.test.ts +0 -320
  61. package/src/put.ts +0 -123
  62. package/src/reopen.test.ts +0 -531
  63. package/src/reopen.ts +0 -190
  64. package/src/scope.test.ts +0 -1421
  65. package/src/scope.ts +0 -1003
  66. package/src/subscribe.test.ts +0 -578
  67. package/src/subscribe.ts +0 -420
  68. package/src/suggest.test.ts +0 -376
  69. package/src/suggest.ts +0 -244
  70. package/src/time.ts +0 -260
  71. package/src/window.test.ts +0 -648
  72. package/src/window.ts +0 -281
  73. package/tsconfig.json +0 -8
@@ -1,403 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import { describe, it } from "node:test";
3
- import type { CalendarSuggestionItem } from "@remit/data-ports";
4
- import {
5
- CalendarInviteAnswer,
6
- CalendarSuggestionSource,
7
- CalendarSuggestionState,
8
- } from "@remit/domain-enums";
9
- import { acceptCalendarSuggestion, buildAcceptedCalendar } from "./accept.js";
10
- import { ical } from "./fixtures.js";
11
- import { MemoryCalendarStore } from "./memory-store.js";
12
- import { provisionDefaultCalendar } from "./put.js";
13
- import { recordCalendarSuggestion } from "./suggest.js";
14
-
15
- const ACCOUNT_CONFIG_ID = "account-config-1";
16
- const ATTENDEE = "user@example.test";
17
- const UID = "invite-4711@example.test";
18
-
19
- const invitation = ({
20
- method = "REQUEST",
21
- sequence = 0,
22
- attendees = ["ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user@example.test"],
23
- extra = [] as string[],
24
- }): string =>
25
- ical(
26
- "BEGIN:VCALENDAR",
27
- "VERSION:2.0",
28
- "PRODID:-//Example Corp//Scheduler//EN",
29
- `METHOD:${method}`,
30
- "BEGIN:VEVENT",
31
- `UID:${UID}`,
32
- "DTSTAMP:20260801T090000Z",
33
- `SEQUENCE:${sequence}`,
34
- "DTSTART:20260901T080000Z",
35
- "DTEND:20260901T090000Z",
36
- "SUMMARY:Quarterly review",
37
- "ORGANIZER:mailto:organizer@example.test",
38
- ...attendees,
39
- "X-EXAMPLE-TICKET:AB-4711",
40
- ...extra,
41
- "END:VEVENT",
42
- "END:VCALENDAR",
43
- );
44
-
45
- const suggestionOf = (
46
- icalData: string,
47
- overrides: Partial<CalendarSuggestionItem> = {},
48
- ): CalendarSuggestionItem => ({
49
- suggestionId: "suggestion-1",
50
- accountConfigId: ACCOUNT_CONFIG_ID,
51
- messageId: "message-1",
52
- bodyPartId: "body-part-1",
53
- icalUid: UID,
54
- sequence: 0,
55
- method: "Request",
56
- source: CalendarSuggestionSource.IcalendarPart,
57
- state: CalendarSuggestionState.Pending,
58
- summary: "Quarterly review",
59
- dtStart: "2026-09-01T08:00:00+00:00",
60
- dtEnd: "2026-09-01T09:00:00+00:00",
61
- endsAtUtc: "2026-09-01T09:00:00Z",
62
- allDay: false,
63
- location: "",
64
- organizer: "organizer@example.test",
65
- zoneCertainty: "Explicit",
66
- icalData,
67
- acceptedCalendarObjectId: "",
68
- supersededByMessageId: "",
69
- createdAt: 0,
70
- updatedAt: 0,
71
- ...overrides,
72
- });
73
-
74
- const provisioned = async (): Promise<{
75
- store: MemoryCalendarStore;
76
- calendarId: string;
77
- }> => {
78
- const store = new MemoryCalendarStore();
79
- const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
80
- return { store, calendarId: collection.calendarId };
81
- };
82
-
83
- const recorded = async (
84
- store: MemoryCalendarStore,
85
- icalData: string,
86
- messageId: string,
87
- ): Promise<CalendarSuggestionItem> => {
88
- const result = await recordCalendarSuggestion(store.calendarSuggestion, {
89
- accountConfigId: ACCOUNT_CONFIG_ID,
90
- messageId,
91
- bodyPartId: "body-part-1",
92
- source: CalendarSuggestionSource.IcalendarPart,
93
- icalData,
94
- timezone: "UTC",
95
- });
96
- assert.ok(result.ok);
97
- return result.value.suggestion;
98
- };
99
-
100
- describe("buildAcceptedCalendar", () => {
101
- it("keeps the UID and the SEQUENCE the organizer sent", async () => {
102
- const built = await buildAcceptedCalendar(
103
- suggestionOf(invitation({ sequence: 3 })),
104
- ATTENDEE,
105
- CalendarInviteAnswer.Accepted,
106
- );
107
-
108
- assert.ok(built.ok);
109
- assert.match(built.value, /UID:invite-4711@example\.test/);
110
- assert.match(built.value, /SEQUENCE:3/);
111
- });
112
-
113
- it("marks the user accepted on the line already naming them", async () => {
114
- const built = await buildAcceptedCalendar(
115
- suggestionOf(invitation({})),
116
- ATTENDEE,
117
- CalendarInviteAnswer.Accepted,
118
- );
119
-
120
- assert.ok(built.ok);
121
- assert.match(built.value, /PARTSTAT=ACCEPTED/);
122
- assert.doesNotMatch(built.value, /NEEDS-ACTION/);
123
- assert.equal(built.value.match(/ATTENDEE/g)?.length, 1);
124
- });
125
-
126
- it("adds the user as an attendee when the invitation never named them", async () => {
127
- const built = await buildAcceptedCalendar(
128
- suggestionOf(invitation({ attendees: [] })),
129
- ATTENDEE,
130
- CalendarInviteAnswer.Accepted,
131
- );
132
-
133
- assert.ok(built.ok);
134
- assert.match(built.value, /ATTENDEE/);
135
- assert.match(built.value, /PARTSTAT=ACCEPTED/);
136
- assert.match(built.value, /mailto:user@example\.test/);
137
- });
138
-
139
- it("drops the METHOD, which a stored resource must not carry", async () => {
140
- // RFC 4791 4.1: a calendar object resource is not a scheduling message.
141
- // Leaving METHOD in makes every client read the user's own entry as an
142
- // unanswered invitation.
143
- const built = await buildAcceptedCalendar(
144
- suggestionOf(invitation({})),
145
- ATTENDEE,
146
- CalendarInviteAnswer.Accepted,
147
- );
148
-
149
- assert.ok(built.ok);
150
- assert.doesNotMatch(built.value, /^METHOD:/m);
151
- });
152
-
153
- it("carries through a property nobody modelled", async () => {
154
- const built = await buildAcceptedCalendar(
155
- suggestionOf(invitation({})),
156
- ATTENDEE,
157
- CalendarInviteAnswer.Accepted,
158
- );
159
-
160
- assert.ok(built.ok);
161
- assert.match(built.value, /X-EXAMPLE-TICKET:AB-4711/);
162
- });
163
-
164
- it("cancels the event when the card is a cancellation", async () => {
165
- const built = await buildAcceptedCalendar(
166
- suggestionOf(invitation({ method: "CANCEL" }), { method: "Cancel" }),
167
- ATTENDEE,
168
- CalendarInviteAnswer.Accepted,
169
- );
170
-
171
- assert.ok(built.ok);
172
- assert.match(built.value, /STATUS:CANCELLED/);
173
- });
174
-
175
- it("refuses a suggestion with no iCalendar behind it", async () => {
176
- const built = await buildAcceptedCalendar(
177
- suggestionOf("", { source: CalendarSuggestionSource.TextHeuristic }),
178
- ATTENDEE,
179
- CalendarInviteAnswer.Accepted,
180
- );
181
-
182
- assert.equal(built.ok, false);
183
- });
184
- });
185
-
186
- describe("acceptCalendarSuggestion", () => {
187
- it("writes the resource and settles the card in one unit", async () => {
188
- const { store, calendarId } = await provisioned();
189
- const suggestion = await recorded(store, invitation({}), "message-1");
190
-
191
- const accepted = await acceptCalendarSuggestion(store, {
192
- accountConfigId: ACCOUNT_CONFIG_ID,
193
- calendarId,
194
- suggestion,
195
- attendee: ATTENDEE,
196
- answer: CalendarInviteAnswer.Accepted,
197
- });
198
-
199
- assert.ok(accepted.ok);
200
- assert.equal(
201
- accepted.value.suggestion.state,
202
- CalendarSuggestionState.Accepted,
203
- );
204
- assert.equal(
205
- accepted.value.suggestion.acceptedCalendarObjectId,
206
- accepted.value.object?.calendarObjectId,
207
- );
208
- assert.equal(accepted.value.object?.icalUid, UID);
209
- assert.equal(store.objects.size, 1);
210
- });
211
-
212
- it("a maybe writes the resource as tentative and settles the card Tentative", async () => {
213
- const { store, calendarId } = await provisioned();
214
- const suggestion = await recorded(store, invitation({}), "message-1");
215
-
216
- const answered = await acceptCalendarSuggestion(store, {
217
- accountConfigId: ACCOUNT_CONFIG_ID,
218
- calendarId,
219
- suggestion,
220
- attendee: ATTENDEE,
221
- answer: CalendarInviteAnswer.Tentative,
222
- });
223
-
224
- assert.ok(answered.ok);
225
- assert.equal(
226
- answered.value.suggestion.state,
227
- CalendarSuggestionState.Tentative,
228
- );
229
- assert.equal(
230
- answered.value.suggestion.acceptedCalendarObjectId,
231
- answered.value.object?.calendarObjectId,
232
- );
233
- assert.match(answered.value.object?.icalData ?? "", /PARTSTAT=TENTATIVE/);
234
- assert.doesNotMatch(answered.value.object?.icalData ?? "", /ACCEPTED/);
235
- assert.equal(store.objects.size, 1);
236
- });
237
-
238
- it("expands the accepted event into the occurrence index", async () => {
239
- const { store, calendarId } = await provisioned();
240
- const suggestion = await recorded(store, invitation({}), "message-1");
241
-
242
- const accepted = await acceptCalendarSuggestion(store, {
243
- accountConfigId: ACCOUNT_CONFIG_ID,
244
- calendarId,
245
- suggestion,
246
- attendee: ATTENDEE,
247
- answer: CalendarInviteAnswer.Accepted,
248
- });
249
-
250
- assert.ok(accepted.ok);
251
- assert.equal(
252
- store.occurrences.get(accepted.value.object?.calendarObjectId ?? "")
253
- ?.length,
254
- 1,
255
- );
256
- });
257
-
258
- it("accepting twice leaves one event in the calendar", async () => {
259
- const { store, calendarId } = await provisioned();
260
- const suggestion = await recorded(store, invitation({}), "message-1");
261
-
262
- const first = await acceptCalendarSuggestion(store, {
263
- accountConfigId: ACCOUNT_CONFIG_ID,
264
- calendarId,
265
- suggestion,
266
- attendee: ATTENDEE,
267
- answer: CalendarInviteAnswer.Accepted,
268
- });
269
- const second = await acceptCalendarSuggestion(store, {
270
- accountConfigId: ACCOUNT_CONFIG_ID,
271
- calendarId,
272
- suggestion,
273
- attendee: ATTENDEE,
274
- answer: CalendarInviteAnswer.Accepted,
275
- });
276
-
277
- assert.ok(first.ok);
278
- assert.ok(second.ok);
279
- assert.equal(store.objects.size, 1);
280
- assert.equal(
281
- second.value.object?.calendarObjectId,
282
- first.value.object?.calendarObjectId,
283
- );
284
- });
285
-
286
- it("accepting a later revision rewrites the event, never a second copy", async () => {
287
- const { store, calendarId } = await provisioned();
288
- const first = await recorded(
289
- store,
290
- invitation({ sequence: 0 }),
291
- "message-1",
292
- );
293
- await acceptCalendarSuggestion(store, {
294
- accountConfigId: ACCOUNT_CONFIG_ID,
295
- calendarId,
296
- suggestion: first,
297
- attendee: ATTENDEE,
298
- answer: CalendarInviteAnswer.Accepted,
299
- });
300
-
301
- const revision = await recorded(
302
- store,
303
- invitation({ sequence: 1 }),
304
- "message-2",
305
- );
306
- const accepted = await acceptCalendarSuggestion(store, {
307
- accountConfigId: ACCOUNT_CONFIG_ID,
308
- calendarId,
309
- suggestion: revision,
310
- attendee: ATTENDEE,
311
- answer: CalendarInviteAnswer.Accepted,
312
- });
313
-
314
- assert.ok(accepted.ok);
315
- assert.equal(store.objects.size, 1);
316
- assert.equal(accepted.value.object?.sequence, 1);
317
- });
318
-
319
- it("never invents an event just to mark it cancelled", async () => {
320
- // The user left the invitation Pending, so nothing is in their calendar.
321
- // Accepting the cancellation must clear the card and write nothing —
322
- // writing a resource here would put a meeting they never had into their
323
- // calendar for the sole purpose of saying it was called off.
324
- const { store, calendarId } = await provisioned();
325
- const request = await recorded(store, invitation({}), "message-1");
326
- const cancel = await recorded(
327
- store,
328
- invitation({ method: "CANCEL", sequence: 1 }),
329
- "message-2",
330
- );
331
-
332
- const accepted = await acceptCalendarSuggestion(store, {
333
- accountConfigId: ACCOUNT_CONFIG_ID,
334
- calendarId,
335
- suggestion: cancel,
336
- attendee: ATTENDEE,
337
- answer: CalendarInviteAnswer.Accepted,
338
- });
339
-
340
- assert.ok(accepted.ok);
341
- assert.equal(accepted.value.outcome, "NothingToCancel");
342
- assert.equal(accepted.value.object, null);
343
- assert.equal(store.objects.size, 0);
344
- assert.equal(
345
- accepted.value.suggestion.state,
346
- CalendarSuggestionState.Dismissed,
347
- );
348
- assert.equal(accepted.value.suggestion.acceptedCalendarObjectId, "");
349
- // The request it superseded is left exactly as the producer left it.
350
- const superseded = await store.calendarSuggestion.get(
351
- ACCOUNT_CONFIG_ID,
352
- request.suggestionId,
353
- );
354
- assert.equal(superseded.state, CalendarSuggestionState.Superseded);
355
- });
356
-
357
- it("a cancellation touches the calendar only once it is accepted", async () => {
358
- const { store, calendarId } = await provisioned();
359
- const request = await recorded(store, invitation({}), "message-1");
360
- await acceptCalendarSuggestion(store, {
361
- accountConfigId: ACCOUNT_CONFIG_ID,
362
- calendarId,
363
- suggestion: request,
364
- attendee: ATTENDEE,
365
- answer: CalendarInviteAnswer.Accepted,
366
- });
367
-
368
- const cancel = await recorded(
369
- store,
370
- invitation({ method: "CANCEL", sequence: 1 }),
371
- "message-2",
372
- );
373
- const before = [...store.objects.values()][0];
374
- assert.equal(before?.status, "Confirmed");
375
-
376
- const accepted = await acceptCalendarSuggestion(store, {
377
- accountConfigId: ACCOUNT_CONFIG_ID,
378
- calendarId,
379
- suggestion: cancel,
380
- attendee: ATTENDEE,
381
- answer: CalendarInviteAnswer.Accepted,
382
- });
383
-
384
- assert.ok(accepted.ok);
385
- assert.equal(accepted.value.object?.status, "Cancelled");
386
- assert.equal(store.objects.size, 1);
387
- });
388
-
389
- it("writes nothing when the invitation's bytes will not parse", async () => {
390
- const { store, calendarId } = await provisioned();
391
-
392
- const accepted = await acceptCalendarSuggestion(store, {
393
- accountConfigId: ACCOUNT_CONFIG_ID,
394
- calendarId,
395
- suggestion: suggestionOf("BEGIN:VCALENDAR"),
396
- attendee: ATTENDEE,
397
- answer: CalendarInviteAnswer.Accepted,
398
- });
399
-
400
- assert.equal(accepted.ok, false);
401
- assert.equal(store.objects.size, 0);
402
- });
403
- });
package/src/accept.ts DELETED
@@ -1,229 +0,0 @@
1
- import type { CalendarInviteAnswer as CalendarInviteAnswerValue } from "@remit/api-openapi-types";
2
- import type {
3
- CalendarObjectItem,
4
- CalendarSuggestionItem,
5
- ICalendarUnitOfWork,
6
- } from "@remit/data-ports";
7
- import {
8
- CalendarInviteAnswer,
9
- CalendarInviteMethod,
10
- CalendarSuggestionState,
11
- } from "@remit/domain-enums";
12
- import ICAL from "ical.js";
13
- import type { CalendarResult } from "./errors.js";
14
- import { parseCalendar, serializeCalendar } from "./parse.js";
15
- import { putCalendarObject } from "./put.js";
16
- import { mailAddressOf } from "./suggest.js";
17
-
18
- const eventsOf = (component: ICAL.Component): ICAL.Component[] =>
19
- component.getAllSubcomponents("vevent");
20
-
21
- const partstatOf: Record<CalendarInviteAnswerValue, string> = {
22
- [CalendarInviteAnswer.Accepted]: "ACCEPTED",
23
- [CalendarInviteAnswer.Tentative]: "TENTATIVE",
24
- };
25
-
26
- const stateOf: Record<
27
- CalendarInviteAnswerValue,
28
- CalendarSuggestionItem["state"]
29
- > = {
30
- [CalendarInviteAnswer.Accepted]: CalendarSuggestionState.Accepted,
31
- [CalendarInviteAnswer.Tentative]: CalendarSuggestionState.Tentative,
32
- };
33
-
34
- /**
35
- * Marks one address with its answer, on every VEVENT of the resource — the
36
- * master and each override alike, since an attendee's answer is to the series.
37
- *
38
- * An ATTENDEE line already naming the user has its PARTSTAT rewritten rather
39
- * than a second one appended: two ATTENDEE lines for one person is a resource
40
- * every other client reads as two people.
41
- */
42
- const markAnswered = (
43
- component: ICAL.Component,
44
- attendee: string,
45
- answer: CalendarInviteAnswerValue,
46
- ): void => {
47
- const wanted = attendee.toLowerCase();
48
- for (const event of eventsOf(component)) {
49
- const existing = event
50
- .getAllProperties("attendee")
51
- .find(
52
- (property) =>
53
- mailAddressOf(String(property.getFirstValue())).toLowerCase() ===
54
- wanted,
55
- );
56
- const property =
57
- existing ?? new ICAL.Property("attendee", event as ICAL.Component);
58
- property.setParameter("partstat", partstatOf[answer]);
59
- if (!existing) {
60
- property.setValue(`mailto:${attendee}`);
61
- event.addProperty(property);
62
- }
63
- }
64
- };
65
-
66
- const markCancelled = (component: ICAL.Component): void => {
67
- for (const event of eventsOf(component)) {
68
- event.removeAllProperties("status");
69
- event.addPropertyWithValue("status", "CANCELLED");
70
- }
71
- };
72
-
73
- /**
74
- * The VCALENDAR a stored resource is made of, built from the invitation's own
75
- * bytes.
76
- *
77
- * Built by editing what arrived rather than by composing a fresh event: the
78
- * UID, the SEQUENCE, the recurrence rule, the overrides and every X- property
79
- * the organizer's client wrote survive into the calendar, which is what makes
80
- * the stored resource answer to the same event a later revision or a
81
- * cancellation names.
82
- *
83
- * The METHOD goes. A scheduling message carries one (RFC 5546); a stored
84
- * calendar object resource must not (RFC 4791 4.1), and leaving it in makes
85
- * every CalDAV client treat the user's own calendar entry as an unanswered
86
- * invitation.
87
- */
88
- export const buildAcceptedCalendar = async (
89
- suggestion: Pick<CalendarSuggestionItem, "icalData" | "method">,
90
- attendee: string,
91
- answer: CalendarInviteAnswerValue,
92
- ): Promise<CalendarResult<string>> => {
93
- const parsed = await parseCalendar(suggestion.icalData);
94
- if (!parsed.ok) return parsed;
95
-
96
- const { component } = parsed.value;
97
- component.removeAllProperties("method");
98
- markAnswered(component, attendee, answer);
99
- if (suggestion.method === CalendarInviteMethod.Cancel) {
100
- markCancelled(component);
101
- }
102
-
103
- return { ok: true, value: serializeCalendar(component) };
104
- };
105
-
106
- export interface AcceptCalendarSuggestionInput {
107
- accountConfigId: string;
108
- calendarId: string;
109
- suggestion: CalendarSuggestionItem;
110
- /** Mail address of the person accepting — the account the message arrived on. */
111
- attendee: string;
112
- answer: CalendarInviteAnswerValue;
113
- }
114
-
115
- /**
116
- * What accepting did.
117
- *
118
- * `Written` is the ordinary case: a resource is in the calendar and `object`
119
- * names it. `NothingToCancel` is a cancellation for a meeting this calendar
120
- * never held — the user never accepted the invitation, or accepted it
121
- * somewhere else — where the only honest act is to clear the card. Writing a
122
- * resource there would put an event in the calendar that exists solely to say
123
- * it was cancelled, which is worse than the meeting the user never had.
124
- */
125
- export type AcceptOutcome = "Written" | "NothingToCancel";
126
-
127
- export interface AcceptedCalendarSuggestion {
128
- suggestion: CalendarSuggestionItem;
129
- outcome: AcceptOutcome;
130
- /** The resource that was written, `null` for `NothingToCancel`. */
131
- object: CalendarObjectItem | null;
132
- }
133
-
134
- /**
135
- * Adds a suggested event to a calendar, as one unit.
136
- *
137
- * The resource is written through `putCalendarObject`, the same function every
138
- * other calendar write goes through, so an event added from a mail and one
139
- * added from the week grid are the same kind of row — and a native client
140
- * editing it afterwards edits the same bytes. The suggestion's own state is
141
- * settled inside that transaction: a suggestion marked `Accepted` with no
142
- * resource behind it, or a resource with the card still asking, is a state
143
- * nothing later can repair.
144
- *
145
- * Accepting a `Cancel` suggestion writes `STATUS:CANCELLED` through that same
146
- * path, but only onto a resource that is already there. Nothing withdraws an
147
- * event on its own — a cancellation reaches the calendar only because a person
148
- * pressed the button, and only when the calendar holds the meeting being
149
- * withdrawn.
150
- *
151
- * No mail is sent. There is no iMIP reply here and none anywhere on this path;
152
- * the organizer learns nothing from the user accepting.
153
- *
154
- * Idempotent. The resource is addressed by the event's UID within the
155
- * collection, so accepting the same suggestion twice — or accepting a later
156
- * revision of an event already added — rewrites the one resource rather than
157
- * leaving the calendar showing the meeting twice.
158
- */
159
- export const acceptCalendarSuggestion = async (
160
- unitOfWork: ICalendarUnitOfWork,
161
- input: AcceptCalendarSuggestionInput,
162
- ): Promise<CalendarResult<AcceptedCalendarSuggestion>> => {
163
- const icalData = await buildAcceptedCalendar(
164
- input.suggestion,
165
- input.attendee,
166
- input.answer,
167
- );
168
- if (!icalData.ok) return icalData;
169
-
170
- return unitOfWork.transaction(async (repos) => {
171
- const existing = await repos.calendarObject.findByUid(
172
- input.calendarId,
173
- input.suggestion.icalUid,
174
- );
175
-
176
- // A cancellation cancels something. With no resource carrying this UID
177
- // there is nothing in the calendar to withdraw, and writing one would
178
- // invent a meeting the user never had purely to mark it cancelled. Clear
179
- // the card and leave the calendar untouched.
180
- if (
181
- input.suggestion.method === CalendarInviteMethod.Cancel &&
182
- existing === null
183
- ) {
184
- const cleared = await repos.calendarSuggestion.settle(
185
- input.accountConfigId,
186
- input.suggestion.suggestionId,
187
- {
188
- state: CalendarSuggestionState.Dismissed,
189
- acceptedCalendarObjectId: "",
190
- },
191
- );
192
- return {
193
- ok: true,
194
- value: {
195
- suggestion: cleared,
196
- outcome: "NothingToCancel",
197
- object: null,
198
- },
199
- };
200
- }
201
-
202
- const written = await putCalendarObject(unitOfWork, {
203
- accountConfigId: input.accountConfigId,
204
- calendarId: input.calendarId,
205
- resourceName:
206
- existing?.resourceName ?? `${input.suggestion.suggestionId}.ics`,
207
- icalData: icalData.value,
208
- });
209
- if (!written.ok) return written;
210
-
211
- const settled = await repos.calendarSuggestion.settle(
212
- input.accountConfigId,
213
- input.suggestion.suggestionId,
214
- {
215
- state: stateOf[input.answer],
216
- acceptedCalendarObjectId: written.value.calendarObjectId,
217
- },
218
- );
219
-
220
- return {
221
- ok: true,
222
- value: {
223
- suggestion: settled,
224
- outcome: "Written",
225
- object: written.value,
226
- },
227
- };
228
- });
229
- };