@remit/calendar-service 0.0.1 → 0.0.2
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/build.test.ts +113 -0
- package/src/build.ts +311 -0
- package/src/errors.ts +13 -1
- package/src/expand.test.ts +10 -0
- package/src/expand.ts +221 -111
- package/src/index.ts +38 -1
- package/src/project.ts +26 -8
- package/src/put.test.ts +17 -0
- package/src/scope.test.ts +472 -0
- package/src/scope.ts +490 -0
- package/src/time.ts +66 -0
- package/src/window.test.ts +524 -0
- package/src/window.ts +232 -0
package/src/expand.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type { CalendarOccurrenceInput } from "@remit/data-ports";
|
|
2
2
|
import ICAL from "ical.js";
|
|
3
3
|
import type { ParsedCalendar } from "./parse.js";
|
|
4
|
-
import { hasRecurrence } from "./project.js";
|
|
5
|
-
import {
|
|
4
|
+
import { hasRecurrence, projectEventDisplay } from "./project.js";
|
|
5
|
+
import {
|
|
6
|
+
dtEndTzid,
|
|
7
|
+
dtStartTzid,
|
|
8
|
+
resolveTime,
|
|
9
|
+
toUtcIso,
|
|
10
|
+
tzidOf,
|
|
11
|
+
} from "./time.js";
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* How far past a series' own start its occurrences are written out.
|
|
@@ -23,6 +29,17 @@ export const CALENDAR_EXPANSION_HORIZON_DAYS = 730;
|
|
|
23
29
|
*/
|
|
24
30
|
export const CALENDAR_EXPANSION_MAX_OCCURRENCES = 1000;
|
|
25
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Ceiling on iterator steps a live window expansion may take.
|
|
34
|
+
*
|
|
35
|
+
* A window expansion walks the series from its own start, because a rule is
|
|
36
|
+
* only meaningful from where it was anchored. A daily event written in 2005 is
|
|
37
|
+
* a few thousand steps away from today, which is cheap; a per-minute one is
|
|
38
|
+
* millions, which is a read that never returns. The cap turns the second case
|
|
39
|
+
* into a short answer rather than a hung request.
|
|
40
|
+
*/
|
|
41
|
+
export const CALENDAR_WINDOW_MAX_STEPS = 50_000;
|
|
42
|
+
|
|
26
43
|
const HORIZON_MS = CALENDAR_EXPANSION_HORIZON_DAYS * 24 * 60 * 60 * 1000;
|
|
27
44
|
|
|
28
45
|
export interface CalendarExpansion {
|
|
@@ -34,14 +51,68 @@ export interface CalendarExpansion {
|
|
|
34
51
|
expandedThrough: string;
|
|
35
52
|
}
|
|
36
53
|
|
|
54
|
+
const occurrenceOf = (
|
|
55
|
+
collectionTimezone: string,
|
|
56
|
+
recurrenceId: string,
|
|
57
|
+
source: ICAL.Component,
|
|
58
|
+
startDate: ICAL.Time,
|
|
59
|
+
endDate: ICAL.Time,
|
|
60
|
+
startTzid: string,
|
|
61
|
+
endTzid: string,
|
|
62
|
+
): CalendarOccurrenceInput => {
|
|
63
|
+
const start = resolveTime(startDate, startTzid, collectionTimezone);
|
|
64
|
+
const end = resolveTime(endDate, endTzid, collectionTimezone);
|
|
65
|
+
return {
|
|
66
|
+
recurrenceId,
|
|
67
|
+
startAt: start.isoUtc,
|
|
68
|
+
endAt: end.isoUtc,
|
|
69
|
+
allDay: start.isDate,
|
|
70
|
+
...projectEventDisplay(source),
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
37
74
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
* The slot each override claims, canonicalized the way the iterator's slots
|
|
76
|
+
* are, so the two can be compared at all.
|
|
77
|
+
*/
|
|
78
|
+
export const overridesBySlot = (
|
|
79
|
+
calendar: ParsedCalendar,
|
|
80
|
+
collectionTimezone: string,
|
|
81
|
+
): Map<string, ICAL.Component> => {
|
|
82
|
+
const bySlot = new Map<string, ICAL.Component>();
|
|
83
|
+
for (const override of calendar.overrides) {
|
|
84
|
+
const recurrenceId = override.getFirstPropertyValue("recurrence-id");
|
|
85
|
+
if (!(recurrenceId instanceof ICAL.Time)) continue;
|
|
86
|
+
const slot = resolveTime(
|
|
87
|
+
recurrenceId,
|
|
88
|
+
tzidOf(override.getFirstProperty("recurrence-id")),
|
|
89
|
+
collectionTimezone,
|
|
90
|
+
).isoUtc;
|
|
91
|
+
bySlot.set(slot, override);
|
|
92
|
+
}
|
|
93
|
+
return bySlot;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** The window a walk collects occurrences in, and what bounds the walk. */
|
|
97
|
+
interface WalkBounds {
|
|
98
|
+
/** Occurrences starting before this are stepped past, not collected. */
|
|
99
|
+
fromMs: number;
|
|
100
|
+
/** The walk stops once a slot starts after this. */
|
|
101
|
+
throughMs: number;
|
|
102
|
+
maxOccurrences: number;
|
|
103
|
+
maxSteps: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface Walk {
|
|
107
|
+
occurrences: CalendarOccurrenceInput[];
|
|
108
|
+
/** Whether the walk stopped on a bound rather than on the series ending. */
|
|
109
|
+
truncated: boolean;
|
|
110
|
+
/** Start of the last slot the iterator itself produced, or `""` for none. */
|
|
111
|
+
lastIteratedStart: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Walks a recurring resource's occurrences between the bounds it is given.
|
|
45
116
|
*
|
|
46
117
|
* EXDATEs are ical.js's business — `ICAL.Event` skips them. Overrides are not
|
|
47
118
|
* left entirely to it: the iterator yields the master's own RRULE and RDATE
|
|
@@ -50,137 +121,176 @@ export interface CalendarExpansion {
|
|
|
50
121
|
* editing the rule — would never be reached. Those are walked explicitly, so
|
|
51
122
|
* every VEVENT in the resource is an occurrence somebody can find.
|
|
52
123
|
*/
|
|
53
|
-
|
|
124
|
+
const walkOccurrences = (
|
|
54
125
|
calendar: ParsedCalendar,
|
|
55
126
|
collectionTimezone: string,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
resolveTime(time, tzid, collectionTimezone);
|
|
59
|
-
|
|
60
|
-
const startTzidOf = (component: ICAL.Component): string =>
|
|
61
|
-
tzidOf(component.getFirstProperty("dtstart"));
|
|
62
|
-
|
|
63
|
-
// DTEND carries its own TZID and need not match DTSTART's, so an end read
|
|
64
|
-
// with the start's zone silently changes the event's length. Only a stated
|
|
65
|
-
// DTEND has a zone of its own: an end ical.js derived from a duration is
|
|
66
|
-
// already in the start's zone, and hinting it with anything else is wrong.
|
|
67
|
-
const endTzidOf = (component: ICAL.Component): string =>
|
|
68
|
-
component.hasProperty("dtend")
|
|
69
|
-
? tzidOf(component.getFirstProperty("dtend"))
|
|
70
|
-
: startTzidOf(component);
|
|
71
|
-
|
|
72
|
-
const occurrenceOf = (
|
|
73
|
-
recurrenceId: string,
|
|
74
|
-
startDate: ICAL.Time,
|
|
75
|
-
endDate: ICAL.Time,
|
|
76
|
-
startTzid: string,
|
|
77
|
-
endTzid: string,
|
|
78
|
-
): CalendarOccurrenceInput => {
|
|
79
|
-
const start = resolve(startDate, startTzid);
|
|
80
|
-
const end = resolve(endDate, endTzid);
|
|
81
|
-
return {
|
|
82
|
-
recurrenceId,
|
|
83
|
-
startAt: start.isoUtc,
|
|
84
|
-
endAt: end.isoUtc,
|
|
85
|
-
allDay: start.isDate,
|
|
86
|
-
};
|
|
87
|
-
};
|
|
88
|
-
|
|
127
|
+
bounds: WalkBounds,
|
|
128
|
+
): Walk => {
|
|
89
129
|
const event = new ICAL.Event(calendar.master);
|
|
90
130
|
for (const override of calendar.overrides) {
|
|
91
131
|
event.relateException(override);
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
occurrences: [
|
|
97
|
-
occurrenceOf(
|
|
98
|
-
"",
|
|
99
|
-
event.startDate,
|
|
100
|
-
event.endDate,
|
|
101
|
-
startTzidOf(calendar.master),
|
|
102
|
-
endTzidOf(calendar.master),
|
|
103
|
-
),
|
|
104
|
-
],
|
|
105
|
-
expandedThrough: "",
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// The slot each override claims, canonicalized the same way the iterator's
|
|
110
|
-
// slots are, so the two can be compared at all.
|
|
111
|
-
const overrideBySlot = new Map<string, ICAL.Component>();
|
|
112
|
-
for (const override of calendar.overrides) {
|
|
113
|
-
const recurrenceId = override.getFirstPropertyValue("recurrence-id");
|
|
114
|
-
if (!(recurrenceId instanceof ICAL.Time)) continue;
|
|
115
|
-
overrideBySlot.set(
|
|
116
|
-
resolve(recurrenceId, tzidOf(override.getFirstProperty("recurrence-id")))
|
|
117
|
-
.isoUtc,
|
|
118
|
-
override,
|
|
119
|
-
);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const seriesStart = resolve(
|
|
123
|
-
event.startDate,
|
|
124
|
-
startTzidOf(calendar.master),
|
|
125
|
-
).instantMs;
|
|
126
|
-
const horizonMs = seriesStart + HORIZON_MS;
|
|
134
|
+
const overrideBySlot = overridesBySlot(calendar, collectionTimezone);
|
|
135
|
+
const masterStartTzid = dtStartTzid(calendar.master);
|
|
127
136
|
const occurrences: CalendarOccurrenceInput[] = [];
|
|
128
137
|
const claimed = new Set<string>();
|
|
129
138
|
const iterator = event.iterator();
|
|
130
139
|
let truncated = false;
|
|
140
|
+
let steps = 0;
|
|
141
|
+
let lastIteratedStart = "";
|
|
131
142
|
|
|
132
143
|
let next = iterator.next();
|
|
133
144
|
while (next) {
|
|
134
|
-
const slot =
|
|
145
|
+
const slot = resolveTime(next, masterStartTzid, collectionTimezone);
|
|
135
146
|
if (
|
|
136
|
-
slot.instantMs >
|
|
137
|
-
occurrences.length >=
|
|
147
|
+
slot.instantMs > bounds.throughMs ||
|
|
148
|
+
occurrences.length >= bounds.maxOccurrences ||
|
|
149
|
+
steps >= bounds.maxSteps
|
|
138
150
|
) {
|
|
139
151
|
truncated = true;
|
|
140
152
|
break;
|
|
141
153
|
}
|
|
142
|
-
|
|
143
|
-
// An overridden slot hands back the override's own DTSTART and DTEND, so
|
|
144
|
-
// the zones to read them in are the override's. A plain slot hands back
|
|
145
|
-
// the rule's start and an end derived from the master's duration, both in
|
|
146
|
-
// the master's start zone.
|
|
147
|
-
const source = overrideBySlot.get(slot.isoUtc);
|
|
148
|
-
const details = event.getOccurrenceDetails(next);
|
|
154
|
+
steps += 1;
|
|
149
155
|
claimed.add(slot.isoUtc);
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
)
|
|
158
|
-
|
|
156
|
+
|
|
157
|
+
if (slot.instantMs >= bounds.fromMs) {
|
|
158
|
+
// An overridden slot hands back the override's own DTSTART and DTEND, so
|
|
159
|
+
// the zones to read them in are the override's. A plain slot hands back
|
|
160
|
+
// the rule's start and an end derived from the master's duration, both in
|
|
161
|
+
// the master's start zone.
|
|
162
|
+
const source = overrideBySlot.get(slot.isoUtc);
|
|
163
|
+
const details = event.getOccurrenceDetails(next);
|
|
164
|
+
occurrences.push(
|
|
165
|
+
occurrenceOf(
|
|
166
|
+
collectionTimezone,
|
|
167
|
+
slot.isoUtc,
|
|
168
|
+
source ?? calendar.master,
|
|
169
|
+
details.startDate,
|
|
170
|
+
details.endDate,
|
|
171
|
+
dtStartTzid(source ?? calendar.master),
|
|
172
|
+
source ? dtEndTzid(source) : masterStartTzid,
|
|
173
|
+
),
|
|
174
|
+
);
|
|
175
|
+
lastIteratedStart = occurrences[occurrences.length - 1]?.startAt ?? "";
|
|
176
|
+
}
|
|
159
177
|
next = iterator.next();
|
|
160
178
|
}
|
|
161
179
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
// Overrides the rule never reached. Written regardless of the horizon: they
|
|
167
|
-
// are a bounded, explicit list, and one moved instance is exactly the thing
|
|
168
|
-
// a user goes looking for.
|
|
180
|
+
// Overrides the rule never reached. Kept regardless of the horizon: they are
|
|
181
|
+
// a bounded, explicit list, and one moved instance is exactly the thing a
|
|
182
|
+
// user goes looking for.
|
|
169
183
|
for (const [slot, override] of overrideBySlot) {
|
|
170
184
|
if (claimed.has(slot)) continue;
|
|
171
185
|
const overrideEvent = new ICAL.Event(override);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
),
|
|
186
|
+
const occurrence = occurrenceOf(
|
|
187
|
+
collectionTimezone,
|
|
188
|
+
slot,
|
|
189
|
+
override,
|
|
190
|
+
overrideEvent.startDate,
|
|
191
|
+
overrideEvent.endDate,
|
|
192
|
+
dtStartTzid(override),
|
|
193
|
+
dtEndTzid(override),
|
|
180
194
|
);
|
|
195
|
+
const startMs = Date.parse(occurrence.startAt);
|
|
196
|
+
if (startMs < bounds.fromMs || startMs > bounds.throughMs) continue;
|
|
197
|
+
occurrences.push(occurrence);
|
|
181
198
|
}
|
|
182
199
|
|
|
183
200
|
occurrences.sort((left, right) => left.startAt.localeCompare(right.startAt));
|
|
184
201
|
|
|
185
|
-
return { occurrences,
|
|
202
|
+
return { occurrences, truncated, lastIteratedStart };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/** The one occurrence a resource that does not recur produces. */
|
|
206
|
+
const singleOccurrence = (
|
|
207
|
+
calendar: ParsedCalendar,
|
|
208
|
+
collectionTimezone: string,
|
|
209
|
+
): CalendarOccurrenceInput => {
|
|
210
|
+
const event = new ICAL.Event(calendar.master);
|
|
211
|
+
return occurrenceOf(
|
|
212
|
+
collectionTimezone,
|
|
213
|
+
"",
|
|
214
|
+
calendar.master,
|
|
215
|
+
event.startDate,
|
|
216
|
+
event.endDate,
|
|
217
|
+
dtStartTzid(calendar.master),
|
|
218
|
+
dtEndTzid(calendar.master),
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Flattens a resource into the occurrence rows a date-range read returns.
|
|
224
|
+
*
|
|
225
|
+
* A non-recurring resource is one row under an empty `recurrenceId`. A
|
|
226
|
+
* recurring one is a row per occurrence, each keyed by its RECURRENCE-ID slot
|
|
227
|
+
* as a UTC instant — the same canonical form an override's own RECURRENCE-ID
|
|
228
|
+
* resolves to, so an override lands on the occurrence it replaces instead of
|
|
229
|
+
* beside it.
|
|
230
|
+
*
|
|
231
|
+
* The horizon is anchored at the series' own start, not at today: two years of
|
|
232
|
+
* a series is a bounded write whatever a caller's clock says. A series running
|
|
233
|
+
* past it comes back marked with `expandedThrough`, which is what tells a later
|
|
234
|
+
* read to expand that resource live instead of trusting the index.
|
|
235
|
+
*/
|
|
236
|
+
export const expandCalendar = (
|
|
237
|
+
calendar: ParsedCalendar,
|
|
238
|
+
collectionTimezone: string,
|
|
239
|
+
): CalendarExpansion => {
|
|
240
|
+
if (!hasRecurrence(calendar)) {
|
|
241
|
+
return {
|
|
242
|
+
occurrences: [singleOccurrence(calendar, collectionTimezone)],
|
|
243
|
+
expandedThrough: "",
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const event = new ICAL.Event(calendar.master);
|
|
248
|
+
const seriesStart = resolveTime(
|
|
249
|
+
event.startDate,
|
|
250
|
+
dtStartTzid(calendar.master),
|
|
251
|
+
collectionTimezone,
|
|
252
|
+
).instantMs;
|
|
253
|
+
|
|
254
|
+
const walk = walkOccurrences(calendar, collectionTimezone, {
|
|
255
|
+
fromMs: Number.NEGATIVE_INFINITY,
|
|
256
|
+
throughMs: seriesStart + HORIZON_MS,
|
|
257
|
+
maxOccurrences: CALENDAR_EXPANSION_MAX_OCCURRENCES,
|
|
258
|
+
maxSteps: Number.POSITIVE_INFINITY,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
occurrences: walk.occurrences,
|
|
263
|
+
expandedThrough:
|
|
264
|
+
walk.truncated && walk.lastIteratedStart
|
|
265
|
+
? toUtcIso(Date.parse(walk.lastIteratedStart))
|
|
266
|
+
: "",
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* The occurrences of a resource that fall inside one window, computed rather
|
|
272
|
+
* than read.
|
|
273
|
+
*
|
|
274
|
+
* This is what serves a view of a series whose stored index stops before the
|
|
275
|
+
* window — an open-ended weekly meeting written years ago, whose horizon ran
|
|
276
|
+
* out long before today. The alternative is extending the index on read, which
|
|
277
|
+
* turns every view of an old series into an unbounded write; a bounded
|
|
278
|
+
* computation that persists nothing is the cheaper half of that trade, and the
|
|
279
|
+
* one that cannot leave anything behind to be wrong later.
|
|
280
|
+
*/
|
|
281
|
+
export const expandCalendarWindow = (
|
|
282
|
+
calendar: ParsedCalendar,
|
|
283
|
+
collectionTimezone: string,
|
|
284
|
+
window: { fromMs: number; toMs: number },
|
|
285
|
+
): CalendarOccurrenceInput[] => {
|
|
286
|
+
if (!hasRecurrence(calendar)) {
|
|
287
|
+
return [singleOccurrence(calendar, collectionTimezone)];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return walkOccurrences(calendar, collectionTimezone, {
|
|
291
|
+
fromMs: window.fromMs,
|
|
292
|
+
throughMs: window.toMs,
|
|
293
|
+
maxOccurrences: CALENDAR_EXPANSION_MAX_OCCURRENCES,
|
|
294
|
+
maxSteps: CALENDAR_WINDOW_MAX_STEPS,
|
|
295
|
+
}).occurrences;
|
|
186
296
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
applyEventFields,
|
|
3
|
+
buildEventCalendar,
|
|
4
|
+
CALENDAR_PRODID,
|
|
5
|
+
type CalendarEventFields,
|
|
6
|
+
eventTimeFields,
|
|
7
|
+
readEventTime,
|
|
8
|
+
readRecurrenceRule,
|
|
9
|
+
} from "./build.js";
|
|
1
10
|
export type {
|
|
2
11
|
CalendarResult,
|
|
3
12
|
CalendarValidationCode,
|
|
@@ -7,8 +16,10 @@ export { computeEtag } from "./etag.js";
|
|
|
7
16
|
export {
|
|
8
17
|
CALENDAR_EXPANSION_HORIZON_DAYS,
|
|
9
18
|
CALENDAR_EXPANSION_MAX_OCCURRENCES,
|
|
19
|
+
CALENDAR_WINDOW_MAX_STEPS,
|
|
10
20
|
type CalendarExpansion,
|
|
11
21
|
expandCalendar,
|
|
22
|
+
expandCalendarWindow,
|
|
12
23
|
} from "./expand.js";
|
|
13
24
|
export {
|
|
14
25
|
type ParsedCalendar,
|
|
@@ -26,4 +37,30 @@ export {
|
|
|
26
37
|
provisionDefaultCalendar,
|
|
27
38
|
putCalendarObject,
|
|
28
39
|
} from "./put.js";
|
|
29
|
-
export {
|
|
40
|
+
export {
|
|
41
|
+
applyScopedDelete,
|
|
42
|
+
applyScopedUpdate,
|
|
43
|
+
findOccurrence,
|
|
44
|
+
type RecurrenceScopeValue,
|
|
45
|
+
type ScopedWrite,
|
|
46
|
+
type ScopedWriteInput,
|
|
47
|
+
} from "./scope.js";
|
|
48
|
+
export {
|
|
49
|
+
civilInZone,
|
|
50
|
+
isResolvableZone,
|
|
51
|
+
type ResolvedTime,
|
|
52
|
+
resolveTime,
|
|
53
|
+
toOffsetIso,
|
|
54
|
+
toUtcIso,
|
|
55
|
+
} from "./time.js";
|
|
56
|
+
export {
|
|
57
|
+
type BusySpan,
|
|
58
|
+
CALENDAR_WINDOW_LOOKBACK_DAYS,
|
|
59
|
+
type CalendarInstance,
|
|
60
|
+
type CalendarWindow,
|
|
61
|
+
type CalendarWindowRepositories,
|
|
62
|
+
isBusy,
|
|
63
|
+
listBusySpans,
|
|
64
|
+
listCalendarInstances,
|
|
65
|
+
mergeBusySpans,
|
|
66
|
+
} from "./window.js";
|
package/src/project.ts
CHANGED
|
@@ -48,6 +48,31 @@ const readString = (component: ICAL.Component, name: string): string => {
|
|
|
48
48
|
* but a master and its exceptions, and every one of those is still an
|
|
49
49
|
* occurrence somebody has to see.
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* The fields that describe one VEVENT rather than the series it belongs to.
|
|
53
|
+
*
|
|
54
|
+
* Read per occurrence as well as per resource: an override VEVENT carries its
|
|
55
|
+
* own summary, status and transparency, and a range read that took them from
|
|
56
|
+
* the master would draw the old title over an edited instance and count a
|
|
57
|
+
* cancelled one as busy time.
|
|
58
|
+
*/
|
|
59
|
+
export type CalendarEventDisplay = Pick<
|
|
60
|
+
CalendarObjectItem,
|
|
61
|
+
"summary" | "status" | "transparency"
|
|
62
|
+
>;
|
|
63
|
+
|
|
64
|
+
export const projectEventDisplay = (
|
|
65
|
+
component: ICAL.Component,
|
|
66
|
+
): CalendarEventDisplay => ({
|
|
67
|
+
summary: readString(component, "summary"),
|
|
68
|
+
status:
|
|
69
|
+
STATUS_BY_ICAL[readString(component, "status").toUpperCase()] ??
|
|
70
|
+
CalendarEventStatus.Confirmed,
|
|
71
|
+
transparency:
|
|
72
|
+
TRANSPARENCY_BY_ICAL[readString(component, "transp").toUpperCase()] ??
|
|
73
|
+
CalendarTransparency.Opaque,
|
|
74
|
+
});
|
|
75
|
+
|
|
51
76
|
export const hasRecurrence = (calendar: ParsedCalendar): boolean =>
|
|
52
77
|
calendar.master.hasProperty("rrule") ||
|
|
53
78
|
calendar.master.hasProperty("rdate") ||
|
|
@@ -85,18 +110,11 @@ export const projectCalendar = (
|
|
|
85
110
|
ok: true,
|
|
86
111
|
value: {
|
|
87
112
|
icalUid: calendar.uid,
|
|
88
|
-
summary: readString(calendar.master, "summary"),
|
|
89
113
|
dtStart: start.isoOffset,
|
|
90
114
|
dtEnd: end.isoOffset,
|
|
91
115
|
allDay: start.isDate,
|
|
92
116
|
zoneCertainty: start.certainty,
|
|
93
|
-
|
|
94
|
-
STATUS_BY_ICAL[readString(calendar.master, "status").toUpperCase()] ??
|
|
95
|
-
CalendarEventStatus.Confirmed,
|
|
96
|
-
transparency:
|
|
97
|
-
TRANSPARENCY_BY_ICAL[
|
|
98
|
-
readString(calendar.master, "transp").toUpperCase()
|
|
99
|
-
] ?? CalendarTransparency.Opaque,
|
|
117
|
+
...projectEventDisplay(calendar.master),
|
|
100
118
|
hasRecurrence: hasRecurrence(calendar),
|
|
101
119
|
sequence: typeof sequence === "number" ? sequence : 0,
|
|
102
120
|
},
|
package/src/put.test.ts
CHANGED
|
@@ -88,6 +88,14 @@ class MemoryCalendarStore implements ICalendarUnitOfWork {
|
|
|
88
88
|
[...this.collections.values()].filter(
|
|
89
89
|
(collection) => collection.accountConfigId === accountConfigId,
|
|
90
90
|
),
|
|
91
|
+
createExclusive: async (input: CreateCalendarCollectionInput) => {
|
|
92
|
+
const calendarId = deriveCalendarId(
|
|
93
|
+
input.accountConfigId,
|
|
94
|
+
input.urlSegment,
|
|
95
|
+
);
|
|
96
|
+
if (this.collections.has(calendarId)) return null;
|
|
97
|
+
return this.collectionRepo.create(input);
|
|
98
|
+
},
|
|
91
99
|
findByUrlSegment: async (accountConfigId: string, urlSegment: string) =>
|
|
92
100
|
this.collections.get(deriveCalendarId(accountConfigId, urlSegment)) ??
|
|
93
101
|
null,
|
|
@@ -119,6 +127,15 @@ class MemoryCalendarStore implements ICalendarUnitOfWork {
|
|
|
119
127
|
this.objects.set(calendarObjectId, object);
|
|
120
128
|
return object;
|
|
121
129
|
},
|
|
130
|
+
listIncompleteExpansions: async (calendarId: string, instant: string) =>
|
|
131
|
+
[...this.objects.values()].filter(
|
|
132
|
+
(object) =>
|
|
133
|
+
object.calendarId === calendarId &&
|
|
134
|
+
object.expandedThrough !== "" &&
|
|
135
|
+
object.expandedThrough < instant,
|
|
136
|
+
),
|
|
137
|
+
find: async (_calendarId: string, calendarObjectId: string) =>
|
|
138
|
+
this.objects.get(calendarObjectId) ?? null,
|
|
122
139
|
get: async (_calendarId: string, calendarObjectId: string) => {
|
|
123
140
|
const object = this.objects.get(calendarObjectId);
|
|
124
141
|
if (!object) throw new MissingRow(calendarObjectId);
|