@remit/calendar-service 0.0.12 → 0.0.13

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/calendar-service",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "iCalendar (RFC 5545) parsing, projection and recurrence expansion over the calendar store",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -20,6 +20,7 @@
20
20
  "test:run": "node $NODE_TEST_FLAGS --import tsx --experimental-test-coverage --test-coverage-include='src/**' --test-coverage-exclude='src/**/*.test.ts' --test-coverage-exclude='src/memory-store.ts' --test-coverage-lines=90 --test 'src/**/*.test.ts'"
21
21
  },
22
22
  "dependencies": {
23
+ "@remit/api-openapi-types": "*",
23
24
  "@remit/data-ports": "*",
24
25
  "@remit/domain-enums": "*",
25
26
  "ical.js": "^2.2.1"
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
2
2
  import { describe, it } from "node:test";
3
3
  import type { CalendarSuggestionItem } from "@remit/data-ports";
4
4
  import {
5
+ CalendarInviteAnswer,
5
6
  CalendarSuggestionSource,
6
7
  CalendarSuggestionState,
7
8
  } from "@remit/domain-enums";
@@ -99,6 +100,7 @@ describe("buildAcceptedCalendar", () => {
99
100
  const built = await buildAcceptedCalendar(
100
101
  suggestionOf(invitation({ sequence: 3 })),
101
102
  ATTENDEE,
103
+ CalendarInviteAnswer.Accepted,
102
104
  );
103
105
 
104
106
  assert.ok(built.ok);
@@ -110,6 +112,7 @@ describe("buildAcceptedCalendar", () => {
110
112
  const built = await buildAcceptedCalendar(
111
113
  suggestionOf(invitation({})),
112
114
  ATTENDEE,
115
+ CalendarInviteAnswer.Accepted,
113
116
  );
114
117
 
115
118
  assert.ok(built.ok);
@@ -122,6 +125,7 @@ describe("buildAcceptedCalendar", () => {
122
125
  const built = await buildAcceptedCalendar(
123
126
  suggestionOf(invitation({ attendees: [] })),
124
127
  ATTENDEE,
128
+ CalendarInviteAnswer.Accepted,
125
129
  );
126
130
 
127
131
  assert.ok(built.ok);
@@ -137,6 +141,7 @@ describe("buildAcceptedCalendar", () => {
137
141
  const built = await buildAcceptedCalendar(
138
142
  suggestionOf(invitation({})),
139
143
  ATTENDEE,
144
+ CalendarInviteAnswer.Accepted,
140
145
  );
141
146
 
142
147
  assert.ok(built.ok);
@@ -147,6 +152,7 @@ describe("buildAcceptedCalendar", () => {
147
152
  const built = await buildAcceptedCalendar(
148
153
  suggestionOf(invitation({})),
149
154
  ATTENDEE,
155
+ CalendarInviteAnswer.Accepted,
150
156
  );
151
157
 
152
158
  assert.ok(built.ok);
@@ -157,6 +163,7 @@ describe("buildAcceptedCalendar", () => {
157
163
  const built = await buildAcceptedCalendar(
158
164
  suggestionOf(invitation({ method: "CANCEL" }), { method: "Cancel" }),
159
165
  ATTENDEE,
166
+ CalendarInviteAnswer.Accepted,
160
167
  );
161
168
 
162
169
  assert.ok(built.ok);
@@ -167,6 +174,7 @@ describe("buildAcceptedCalendar", () => {
167
174
  const built = await buildAcceptedCalendar(
168
175
  suggestionOf("", { source: CalendarSuggestionSource.TextHeuristic }),
169
176
  ATTENDEE,
177
+ CalendarInviteAnswer.Accepted,
170
178
  );
171
179
 
172
180
  assert.equal(built.ok, false);
@@ -183,6 +191,7 @@ describe("acceptCalendarSuggestion", () => {
183
191
  calendarId,
184
192
  suggestion,
185
193
  attendee: ATTENDEE,
194
+ answer: CalendarInviteAnswer.Accepted,
186
195
  });
187
196
 
188
197
  assert.ok(accepted.ok);
@@ -198,6 +207,32 @@ describe("acceptCalendarSuggestion", () => {
198
207
  assert.equal(store.objects.size, 1);
199
208
  });
200
209
 
210
+ it("a maybe writes the resource as tentative and settles the card Tentative", async () => {
211
+ const { store, calendarId } = await provisioned();
212
+ const suggestion = await recorded(store, invitation({}), "message-1");
213
+
214
+ const answered = await acceptCalendarSuggestion(store, {
215
+ accountConfigId: ACCOUNT_CONFIG_ID,
216
+ calendarId,
217
+ suggestion,
218
+ attendee: ATTENDEE,
219
+ answer: CalendarInviteAnswer.Tentative,
220
+ });
221
+
222
+ assert.ok(answered.ok);
223
+ assert.equal(
224
+ answered.value.suggestion.state,
225
+ CalendarSuggestionState.Tentative,
226
+ );
227
+ assert.equal(
228
+ answered.value.suggestion.acceptedCalendarObjectId,
229
+ answered.value.object?.calendarObjectId,
230
+ );
231
+ assert.match(answered.value.object?.icalData ?? "", /PARTSTAT=TENTATIVE/);
232
+ assert.doesNotMatch(answered.value.object?.icalData ?? "", /ACCEPTED/);
233
+ assert.equal(store.objects.size, 1);
234
+ });
235
+
201
236
  it("expands the accepted event into the occurrence index", async () => {
202
237
  const { store, calendarId } = await provisioned();
203
238
  const suggestion = await recorded(store, invitation({}), "message-1");
@@ -207,6 +242,7 @@ describe("acceptCalendarSuggestion", () => {
207
242
  calendarId,
208
243
  suggestion,
209
244
  attendee: ATTENDEE,
245
+ answer: CalendarInviteAnswer.Accepted,
210
246
  });
211
247
 
212
248
  assert.ok(accepted.ok);
@@ -226,12 +262,14 @@ describe("acceptCalendarSuggestion", () => {
226
262
  calendarId,
227
263
  suggestion,
228
264
  attendee: ATTENDEE,
265
+ answer: CalendarInviteAnswer.Accepted,
229
266
  });
230
267
  const second = await acceptCalendarSuggestion(store, {
231
268
  accountConfigId: ACCOUNT_CONFIG_ID,
232
269
  calendarId,
233
270
  suggestion,
234
271
  attendee: ATTENDEE,
272
+ answer: CalendarInviteAnswer.Accepted,
235
273
  });
236
274
 
237
275
  assert.ok(first.ok);
@@ -255,6 +293,7 @@ describe("acceptCalendarSuggestion", () => {
255
293
  calendarId,
256
294
  suggestion: first,
257
295
  attendee: ATTENDEE,
296
+ answer: CalendarInviteAnswer.Accepted,
258
297
  });
259
298
 
260
299
  const revision = await recorded(
@@ -267,6 +306,7 @@ describe("acceptCalendarSuggestion", () => {
267
306
  calendarId,
268
307
  suggestion: revision,
269
308
  attendee: ATTENDEE,
309
+ answer: CalendarInviteAnswer.Accepted,
270
310
  });
271
311
 
272
312
  assert.ok(accepted.ok);
@@ -292,6 +332,7 @@ describe("acceptCalendarSuggestion", () => {
292
332
  calendarId,
293
333
  suggestion: cancel,
294
334
  attendee: ATTENDEE,
335
+ answer: CalendarInviteAnswer.Accepted,
295
336
  });
296
337
 
297
338
  assert.ok(accepted.ok);
@@ -319,6 +360,7 @@ describe("acceptCalendarSuggestion", () => {
319
360
  calendarId,
320
361
  suggestion: request,
321
362
  attendee: ATTENDEE,
363
+ answer: CalendarInviteAnswer.Accepted,
322
364
  });
323
365
 
324
366
  const cancel = await recorded(
@@ -334,6 +376,7 @@ describe("acceptCalendarSuggestion", () => {
334
376
  calendarId,
335
377
  suggestion: cancel,
336
378
  attendee: ATTENDEE,
379
+ answer: CalendarInviteAnswer.Accepted,
337
380
  });
338
381
 
339
382
  assert.ok(accepted.ok);
@@ -349,6 +392,7 @@ describe("acceptCalendarSuggestion", () => {
349
392
  calendarId,
350
393
  suggestion: suggestionOf("BEGIN:VCALENDAR"),
351
394
  attendee: ATTENDEE,
395
+ answer: CalendarInviteAnswer.Accepted,
352
396
  });
353
397
 
354
398
  assert.equal(accepted.ok, false);
package/src/accept.ts CHANGED
@@ -1,9 +1,11 @@
1
+ import type { CalendarInviteAnswer as CalendarInviteAnswerValue } from "@remit/api-openapi-types";
1
2
  import type {
2
3
  CalendarObjectItem,
3
4
  CalendarSuggestionItem,
4
5
  ICalendarUnitOfWork,
5
6
  } from "@remit/data-ports";
6
7
  import {
8
+ CalendarInviteAnswer,
7
9
  CalendarInviteMethod,
8
10
  CalendarSuggestionState,
9
11
  } from "@remit/domain-enums";
@@ -16,15 +18,32 @@ import { mailAddressOf } from "./suggest.js";
16
18
  const eventsOf = (component: ICAL.Component): ICAL.Component[] =>
17
19
  component.getAllSubcomponents("vevent");
18
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
+
19
34
  /**
20
- * Marks one address as having accepted, on every VEVENT of the resource — the
35
+ * Marks one address with its answer, on every VEVENT of the resource — the
21
36
  * master and each override alike, since an attendee's answer is to the series.
22
37
  *
23
38
  * An ATTENDEE line already naming the user has its PARTSTAT rewritten rather
24
39
  * than a second one appended: two ATTENDEE lines for one person is a resource
25
40
  * every other client reads as two people.
26
41
  */
27
- const markAccepted = (component: ICAL.Component, attendee: string): void => {
42
+ const markAnswered = (
43
+ component: ICAL.Component,
44
+ attendee: string,
45
+ answer: CalendarInviteAnswerValue,
46
+ ): void => {
28
47
  const wanted = attendee.toLowerCase();
29
48
  for (const event of eventsOf(component)) {
30
49
  const existing = event
@@ -36,7 +55,7 @@ const markAccepted = (component: ICAL.Component, attendee: string): void => {
36
55
  );
37
56
  const property =
38
57
  existing ?? new ICAL.Property("attendee", event as ICAL.Component);
39
- property.setParameter("partstat", "ACCEPTED");
58
+ property.setParameter("partstat", partstatOf[answer]);
40
59
  if (!existing) {
41
60
  property.setValue(`mailto:${attendee}`);
42
61
  event.addProperty(property);
@@ -69,13 +88,14 @@ const markCancelled = (component: ICAL.Component): void => {
69
88
  export const buildAcceptedCalendar = async (
70
89
  suggestion: Pick<CalendarSuggestionItem, "icalData" | "method">,
71
90
  attendee: string,
91
+ answer: CalendarInviteAnswerValue,
72
92
  ): Promise<CalendarResult<string>> => {
73
93
  const parsed = await parseCalendar(suggestion.icalData);
74
94
  if (!parsed.ok) return parsed;
75
95
 
76
96
  const { component } = parsed.value;
77
97
  component.removeAllProperties("method");
78
- markAccepted(component, attendee);
98
+ markAnswered(component, attendee, answer);
79
99
  if (suggestion.method === CalendarInviteMethod.Cancel) {
80
100
  markCancelled(component);
81
101
  }
@@ -89,6 +109,7 @@ export interface AcceptCalendarSuggestionInput {
89
109
  suggestion: CalendarSuggestionItem;
90
110
  /** Mail address of the person accepting — the account the message arrived on. */
91
111
  attendee: string;
112
+ answer: CalendarInviteAnswerValue;
92
113
  }
93
114
 
94
115
  /**
@@ -142,6 +163,7 @@ export const acceptCalendarSuggestion = async (
142
163
  const icalData = await buildAcceptedCalendar(
143
164
  input.suggestion,
144
165
  input.attendee,
166
+ input.answer,
145
167
  );
146
168
  if (!icalData.ok) return icalData;
147
169
 
@@ -190,7 +212,7 @@ export const acceptCalendarSuggestion = async (
190
212
  input.accountConfigId,
191
213
  input.suggestion.suggestionId,
192
214
  {
193
- state: CalendarSuggestionState.Accepted,
215
+ state: stateOf[input.answer],
194
216
  acceptedCalendarObjectId: written.value.calendarObjectId,
195
217
  },
196
218
  );