@remit/calendar-service 0.0.10 → 0.0.11

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.10",
3
+ "version": "0.0.11",
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",
package/src/index.ts CHANGED
@@ -93,5 +93,6 @@ export {
93
93
  isBusy,
94
94
  listBusySpans,
95
95
  listCalendarInstances,
96
+ listObjectInstances,
96
97
  mergeBusySpans,
97
98
  } from "./window.js";
@@ -20,6 +20,7 @@ import {
20
20
  type CalendarWindowRepositories,
21
21
  listBusySpans,
22
22
  listCalendarInstances,
23
+ listObjectInstances,
23
24
  mergeBusySpans,
24
25
  } from "./window.js";
25
26
 
@@ -369,6 +370,78 @@ describe("listCalendarInstances", () => {
369
370
  });
370
371
  });
371
372
 
373
+ describe("listObjectInstances", () => {
374
+ const ownRows = (resource: StoredResource) => ({
375
+ calendarEventIndex: {
376
+ listForObject: async () => resource.rows,
377
+ },
378
+ });
379
+
380
+ it("reads one resource's own rows where its index reaches the window", async () => {
381
+ const calendar = collection();
382
+ const weekly = await store(
383
+ calendar,
384
+ "weekly.ics",
385
+ ical(
386
+ "BEGIN:VCALENDAR",
387
+ "VERSION:2.0",
388
+ "BEGIN:VEVENT",
389
+ "UID:weekly@example.com",
390
+ "DTSTART:20260907T090000Z",
391
+ "DTEND:20260907T100000Z",
392
+ "SUMMARY:Stand-up",
393
+ "RRULE:FREQ=WEEKLY;COUNT=4",
394
+ "END:VEVENT",
395
+ "END:VCALENDAR",
396
+ ),
397
+ );
398
+
399
+ const instances = await listObjectInstances(
400
+ ownRows(weekly),
401
+ calendar,
402
+ weekly.object,
403
+ { from: "2026-09-10T00:00:00Z", to: "2026-09-25T00:00:00Z" },
404
+ );
405
+
406
+ assert.deepEqual(
407
+ instances.map((instance) => instance.startAt),
408
+ ["2026-09-14T09:00:00Z", "2026-09-21T09:00:00Z"],
409
+ );
410
+ });
411
+
412
+ it("expands the resource live where its index stops before the window", async () => {
413
+ const calendar = collection();
414
+ const openEnded = await store(
415
+ calendar,
416
+ "open.ics",
417
+ ical(
418
+ "BEGIN:VCALENDAR",
419
+ "VERSION:2.0",
420
+ "BEGIN:VEVENT",
421
+ "UID:open@example.com",
422
+ "DTSTART:20200907T090000Z",
423
+ "DTEND:20200907T100000Z",
424
+ "SUMMARY:Weekly one-to-one",
425
+ "RRULE:FREQ=WEEKLY",
426
+ "END:VEVENT",
427
+ "END:VCALENDAR",
428
+ ),
429
+ );
430
+
431
+ const instances = await listObjectInstances(
432
+ ownRows(openEnded),
433
+ calendar,
434
+ openEnded.object,
435
+ { from: "2026-09-14T00:00:00Z", to: "2026-09-28T00:00:00Z" },
436
+ );
437
+
438
+ assert.deepEqual(
439
+ instances.map((instance) => instance.startAt),
440
+ ["2026-09-14T09:00:00Z", "2026-09-21T09:00:00Z"],
441
+ );
442
+ });
443
+ });
444
+
372
445
  describe("mergeBusySpans", () => {
373
446
  it("collapses overlapping and touching spans", () => {
374
447
  assert.deepEqual(
package/src/window.ts CHANGED
@@ -158,6 +158,55 @@ export const listCalendarInstances = async (
158
158
  .sort((left, right) => left.startAt.localeCompare(right.startAt));
159
159
  };
160
160
 
161
+ /**
162
+ * The occurrences of one resource in a window, read from that resource alone:
163
+ * its own index rows where the index reaches the window, and a live expansion
164
+ * of it where it does not.
165
+ */
166
+ export const listObjectInstances = async (
167
+ repositories: {
168
+ calendarEventIndex: Pick<ICalendarEventIndexRepository, "listForObject">;
169
+ },
170
+ collection: CalendarCollectionItem,
171
+ object: CalendarObjectItem,
172
+ window: CalendarWindow,
173
+ ): Promise<CalendarInstance[]> => {
174
+ const fromMs = Date.parse(window.from);
175
+ const toMs = Date.parse(window.to);
176
+ const indexReaches =
177
+ object.expandedThrough === "" || object.expandedThrough >= window.to;
178
+
179
+ const occurrences = indexReaches
180
+ ? await repositories.calendarEventIndex.listForObject(
181
+ collection.calendarId,
182
+ object.calendarObjectId,
183
+ )
184
+ : await expandObject(object, collection.timezone, fromMs, toMs);
185
+
186
+ return occurrences
187
+ .map((occurrence) => instanceOf(collection, object, occurrence))
188
+ .filter((instance) => overlapsWindow(instance, fromMs, toMs))
189
+ .sort((left, right) => left.startAt.localeCompare(right.startAt));
190
+ };
191
+
192
+ const expandObject = async (
193
+ object: CalendarObjectItem,
194
+ timezone: string,
195
+ fromMs: number,
196
+ toMs: number,
197
+ ): Promise<CalendarOccurrenceInput[]> => {
198
+ const parsed = await parseCalendar(object.icalData);
199
+ if (!parsed.ok) {
200
+ throw new Error(
201
+ `stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`,
202
+ );
203
+ }
204
+ return expandCalendarWindow(parsed.value, timezone, {
205
+ fromMs: fromMs - LOOKBACK_MS,
206
+ toMs,
207
+ });
208
+ };
209
+
161
210
  const overlapsWindow = (
162
211
  instance: CalendarInstance,
163
212
  fromMs: number,