@remit/calendar-service 0.0.16 → 0.0.17
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 +1 -1
- package/src/accept.test.ts +1 -0
- package/src/memory-store.ts +7 -1
- package/src/suggest.test.ts +25 -0
- package/src/suggest.ts +21 -0
package/package.json
CHANGED
package/src/accept.test.ts
CHANGED
package/src/memory-store.ts
CHANGED
|
@@ -273,11 +273,17 @@ export class MemoryCalendarStore implements ICalendarUnitOfWork {
|
|
|
273
273
|
listByState: async (
|
|
274
274
|
accountConfigId: string,
|
|
275
275
|
state: CalendarSuggestionItem["state"],
|
|
276
|
+
options: {
|
|
277
|
+
endsAfter: string;
|
|
278
|
+
limit?: number;
|
|
279
|
+
continuationToken?: string;
|
|
280
|
+
},
|
|
276
281
|
) => ({
|
|
277
282
|
items: [...this.suggestions.values()].filter(
|
|
278
283
|
(suggestion) =>
|
|
279
284
|
suggestion.accountConfigId === accountConfigId &&
|
|
280
|
-
suggestion.state === state
|
|
285
|
+
suggestion.state === state &&
|
|
286
|
+
suggestion.endsAtUtc >= options.endsAfter,
|
|
281
287
|
),
|
|
282
288
|
continuationToken: undefined,
|
|
283
289
|
}),
|
package/src/suggest.test.ts
CHANGED
|
@@ -71,9 +71,32 @@ describe("projectSuggestion", () => {
|
|
|
71
71
|
assert.equal(projected.value.organizer, "organizer@example.test");
|
|
72
72
|
assert.equal(projected.value.dtStart, "2026-09-01T10:00:00+02:00");
|
|
73
73
|
assert.equal(projected.value.dtEnd, "2026-09-01T11:00:00+02:00");
|
|
74
|
+
assert.equal(projected.value.endsAtUtc, "2026-09-01T09:00:00Z");
|
|
74
75
|
assert.equal(projected.value.allDay, false);
|
|
75
76
|
});
|
|
76
77
|
|
|
78
|
+
const withRule = (rule: string): string => invitation({ extra: [rule] });
|
|
79
|
+
|
|
80
|
+
it("ends a bounded series with its last occurrence", async () => {
|
|
81
|
+
const projected = await projectSuggestion(
|
|
82
|
+
withRule("RRULE:FREQ=WEEKLY;COUNT=3"),
|
|
83
|
+
"Europe/Amsterdam",
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
assert.ok(projected.ok);
|
|
87
|
+
assert.equal(projected.value.endsAtUtc, "2026-09-15T09:00:00Z");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("never ends an open-ended series", async () => {
|
|
91
|
+
const projected = await projectSuggestion(
|
|
92
|
+
withRule("RRULE:FREQ=WEEKLY"),
|
|
93
|
+
"Europe/Amsterdam",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
assert.ok(projected.ok);
|
|
97
|
+
assert.equal(projected.value.endsAtUtc, "9999-12-31T23:59:59Z");
|
|
98
|
+
});
|
|
99
|
+
|
|
77
100
|
it("reads a cancellation as a cancellation", async () => {
|
|
78
101
|
const projected = await projectSuggestion(
|
|
79
102
|
invitation({ method: "CANCEL", sequence: 2 }),
|
|
@@ -289,10 +312,12 @@ describe("recordCalendarSuggestion", () => {
|
|
|
289
312
|
listByState: async (
|
|
290
313
|
accountConfigId: string,
|
|
291
314
|
state: CalendarSuggestionItem["state"],
|
|
315
|
+
options: { endsAfter: string },
|
|
292
316
|
) => {
|
|
293
317
|
const page = await store.calendarSuggestion.listByState(
|
|
294
318
|
accountConfigId,
|
|
295
319
|
state,
|
|
320
|
+
options,
|
|
296
321
|
);
|
|
297
322
|
// The user accepts, right here.
|
|
298
323
|
await store.calendarSuggestion.settle(ACCOUNT_CONFIG_ID, target, {
|
package/src/suggest.ts
CHANGED
|
@@ -9,8 +9,14 @@ import {
|
|
|
9
9
|
} from "@remit/domain-enums";
|
|
10
10
|
import type ICAL from "ical.js";
|
|
11
11
|
import type { CalendarResult } from "./errors.js";
|
|
12
|
+
import { expandCalendar } from "./expand.js";
|
|
13
|
+
import type { ParsedCalendar } from "./parse.js";
|
|
12
14
|
import { parseCalendar } from "./parse.js";
|
|
13
15
|
import { projectCalendar, readString } from "./project.js";
|
|
16
|
+
import { toUtcIso } from "./time.js";
|
|
17
|
+
|
|
18
|
+
const OPEN_ENDED_SERIES = "9999-12-31T23:59:59Z";
|
|
19
|
+
const EARLIEST_INSTANT = new Date(0).toISOString();
|
|
14
20
|
|
|
15
21
|
const METHOD_BY_ICAL: Record<string, CalendarSuggestionItem["method"]> = {
|
|
16
22
|
REQUEST: CalendarInviteMethod.Request,
|
|
@@ -48,12 +54,25 @@ export type CalendarSuggestionProjection = Pick<
|
|
|
48
54
|
| "summary"
|
|
49
55
|
| "dtStart"
|
|
50
56
|
| "dtEnd"
|
|
57
|
+
| "endsAtUtc"
|
|
51
58
|
| "allDay"
|
|
52
59
|
| "location"
|
|
53
60
|
| "organizer"
|
|
54
61
|
| "zoneCertainty"
|
|
55
62
|
>;
|
|
56
63
|
|
|
64
|
+
const endsAtUtcOf = (
|
|
65
|
+
calendar: ParsedCalendar,
|
|
66
|
+
timezone: string,
|
|
67
|
+
dtEnd: string,
|
|
68
|
+
): string => {
|
|
69
|
+
const expansion = expandCalendar(calendar, timezone);
|
|
70
|
+
if (expansion.expandedThrough !== "") return OPEN_ENDED_SERIES;
|
|
71
|
+
const ends = expansion.occurrences.map((occurrence) => occurrence.endAt);
|
|
72
|
+
if (ends.length === 0) return toUtcIso(Date.parse(dtEnd));
|
|
73
|
+
return ends.reduce((latest, end) => (end > latest ? end : latest));
|
|
74
|
+
};
|
|
75
|
+
|
|
57
76
|
/**
|
|
58
77
|
* Reads a message's `text/calendar` bytes into the facts a card is drawn from.
|
|
59
78
|
*
|
|
@@ -80,6 +99,7 @@ export const projectSuggestion = async (
|
|
|
80
99
|
summary: projection.value.summary,
|
|
81
100
|
dtStart: projection.value.dtStart,
|
|
82
101
|
dtEnd: projection.value.dtEnd,
|
|
102
|
+
endsAtUtc: endsAtUtcOf(parsed.value, timezone, projection.value.dtEnd),
|
|
83
103
|
allDay: projection.value.allDay,
|
|
84
104
|
location: readString(parsed.value.master, "location"),
|
|
85
105
|
organizer: mailAddressOf(readString(parsed.value.master, "organizer")),
|
|
@@ -125,6 +145,7 @@ const listInState = async (
|
|
|
125
145
|
let continuationToken: string | undefined;
|
|
126
146
|
do {
|
|
127
147
|
const page = await repo.listByState(accountConfigId, state, {
|
|
148
|
+
endsAfter: EARLIEST_INSTANT,
|
|
128
149
|
continuationToken,
|
|
129
150
|
});
|
|
130
151
|
items.push(...page.items);
|