@remit/calendar-service 0.0.18 → 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 -250
  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 -128
  55. package/src/memory-store.ts +0 -485
  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 -318
  61. package/src/put.ts +0 -133
  62. package/src/reopen.test.ts +0 -529
  63. package/src/reopen.ts +0 -199
  64. package/src/scope.test.ts +0 -1421
  65. package/src/scope.ts +0 -1003
  66. package/src/subscribe.test.ts +0 -582
  67. package/src/subscribe.ts +0 -433
  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
package/dist/put.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import type { CalendarCollectionItem, CalendarObjectItem, DeleteCalendarObjectWrite, ICalendarStore, PutCalendarObjectWrite } from "@remit/data-ports";
2
+ import type { CalendarResult } from "./errors.js";
3
+ /** URL segment of the collection every account config is provisioned with. */
4
+ export declare const DEFAULT_CALENDAR_URL_SEGMENT = "default";
5
+ export interface PutCalendarObjectInput {
6
+ accountConfigId: string;
7
+ calendarId: string;
8
+ /** Last path segment of the resource's URL, e.g. `"a1b2c3.ics"`. */
9
+ resourceName: string;
10
+ /** The VCALENDAR text as it arrived, stored byte-for-byte. */
11
+ icalData: string;
12
+ }
13
+ /**
14
+ * Turns one calendar resource into the write that stores it, read in the
15
+ * collection's timezone. Every caller that stores an event goes through here —
16
+ * a REST handler, a DAV PUT, an accepted suggestion, a feed refresh — because
17
+ * the object row, its occurrence rows and the collection's sequence bump are
18
+ * one fact, and a caller that writes any of them alone leaves the store
19
+ * describing a calendar that does not exist.
20
+ *
21
+ * The bytes are stored exactly as given: parsing is for validating and
22
+ * projecting them, never for rewriting them. Refusing the resource is a
23
+ * returned value — the input is client-supplied, so malformed iCalendar is an
24
+ * outcome the caller renders, not a fault.
25
+ */
26
+ export declare const prepareCalendarObject: (collection: Pick<CalendarCollectionItem, "timezone">, input: PutCalendarObjectInput) => Promise<CalendarResult<PutCalendarObjectWrite>>;
27
+ /** The resource a committed put wrote, under the id its name derives to. */
28
+ export declare const readWrittenObject: (store: ICalendarStore, write: PutCalendarObjectWrite) => Promise<CalendarObjectItem>;
29
+ /** Writes one calendar resource into its collection. */
30
+ export declare const putCalendarObject: (store: ICalendarStore, input: PutCalendarObjectInput) => Promise<CalendarResult<CalendarObjectItem>>;
31
+ /**
32
+ * Removes a resource and the occurrences it produced — an object dropped
33
+ * without its occurrence rows leaves a calendar showing events no resource
34
+ * backs, which nothing later can attribute or clean up.
35
+ */
36
+ export declare const deleteCalendarObjectWrite: (input: {
37
+ accountConfigId: string;
38
+ calendarId: string;
39
+ calendarObjectId: string;
40
+ }) => DeleteCalendarObjectWrite;
41
+ export declare const deleteCalendarObject: (store: ICalendarStore, input: {
42
+ accountConfigId: string;
43
+ calendarId: string;
44
+ calendarObjectId: string;
45
+ }) => Promise<void>;
46
+ /**
47
+ * The collection an account config stores events in until someone makes
48
+ * another. Safe to call on every request that needs a calendar: `calendarId` is
49
+ * derived from the account config and the URL segment, so a second call returns
50
+ * the collection the first one made rather than a rival copy of it.
51
+ */
52
+ export declare const provisionDefaultCalendar: (store: ICalendarStore, accountConfigId: string, displayName?: string) => Promise<CalendarCollectionItem>;
package/dist/put.js ADDED
@@ -0,0 +1,78 @@
1
+ import { deriveCalendarObjectId } from "@remit/data-ports/id";
2
+ import { CalendarSource } from "@remit/domain-enums";
3
+ import { computeEtag } from "./etag.js";
4
+ import { expandCalendar } from "./expand.js";
5
+ import { parseCalendar } from "./parse.js";
6
+ import { projectCalendar } from "./project.js";
7
+ /** URL segment of the collection every account config is provisioned with. */
8
+ export const DEFAULT_CALENDAR_URL_SEGMENT = "default";
9
+ /**
10
+ * Turns one calendar resource into the write that stores it, read in the
11
+ * collection's timezone. Every caller that stores an event goes through here —
12
+ * a REST handler, a DAV PUT, an accepted suggestion, a feed refresh — because
13
+ * the object row, its occurrence rows and the collection's sequence bump are
14
+ * one fact, and a caller that writes any of them alone leaves the store
15
+ * describing a calendar that does not exist.
16
+ *
17
+ * The bytes are stored exactly as given: parsing is for validating and
18
+ * projecting them, never for rewriting them. Refusing the resource is a
19
+ * returned value — the input is client-supplied, so malformed iCalendar is an
20
+ * outcome the caller renders, not a fault.
21
+ */
22
+ export const prepareCalendarObject = async (collection, input) => {
23
+ const parsed = await parseCalendar(input.icalData);
24
+ if (!parsed.ok)
25
+ return parsed;
26
+ const projection = projectCalendar(parsed.value, collection.timezone);
27
+ if (!projection.ok)
28
+ return projection;
29
+ const expansion = expandCalendar(parsed.value, collection.timezone);
30
+ return {
31
+ ok: true,
32
+ value: {
33
+ kind: "PutObject",
34
+ accountConfigId: input.accountConfigId,
35
+ object: {
36
+ ...projection.value,
37
+ calendarId: input.calendarId,
38
+ resourceName: input.resourceName,
39
+ icalData: input.icalData,
40
+ etag: computeEtag(input.icalData),
41
+ expandedThrough: expansion.expandedThrough,
42
+ },
43
+ occurrences: expansion.occurrences,
44
+ },
45
+ };
46
+ };
47
+ /** The resource a committed put wrote, under the id its name derives to. */
48
+ export const readWrittenObject = (store, write) => store.repos.calendarObject.get(write.object.calendarId, deriveCalendarObjectId(write.object.calendarId, write.object.resourceName));
49
+ /** Writes one calendar resource into its collection. */
50
+ export const putCalendarObject = async (store, input) => {
51
+ const collection = await store.repos.calendarCollection.get(input.accountConfigId, input.calendarId);
52
+ const write = await prepareCalendarObject(collection, input);
53
+ if (!write.ok)
54
+ return write;
55
+ await store.commit([write.value]);
56
+ return { ok: true, value: await readWrittenObject(store, write.value) };
57
+ };
58
+ /**
59
+ * Removes a resource and the occurrences it produced — an object dropped
60
+ * without its occurrence rows leaves a calendar showing events no resource
61
+ * backs, which nothing later can attribute or clean up.
62
+ */
63
+ export const deleteCalendarObjectWrite = (input) => ({ kind: "DeleteObject", ...input });
64
+ export const deleteCalendarObject = async (store, input) => {
65
+ await store.commit([deleteCalendarObjectWrite(input)]);
66
+ };
67
+ /**
68
+ * The collection an account config stores events in until someone makes
69
+ * another. Safe to call on every request that needs a calendar: `calendarId` is
70
+ * derived from the account config and the URL segment, so a second call returns
71
+ * the collection the first one made rather than a rival copy of it.
72
+ */
73
+ export const provisionDefaultCalendar = (store, accountConfigId, displayName = "Calendar") => store.repos.calendarCollection.create({
74
+ accountConfigId,
75
+ urlSegment: DEFAULT_CALENDAR_URL_SEGMENT,
76
+ displayName,
77
+ source: CalendarSource.Default,
78
+ });
@@ -0,0 +1,14 @@
1
+ import type { CalendarAnswerOvertaken as CalendarAnswerOvertakenValue } from "@remit/api-openapi-types";
2
+ import type { CalendarSuggestionItem, ICalendarStore, ICalendarSuggestionRepository } from "@remit/data-ports";
3
+ import { type CalendarResult } from "./errors.js";
4
+ export interface ReopenCalendarSuggestionInput {
5
+ accountConfigId: string;
6
+ suggestion: CalendarSuggestionItem;
7
+ }
8
+ type SuggestionHolders = Pick<ICalendarSuggestionRepository, "listByAcceptedCalendarObjects">;
9
+ export declare const answersOvertakenBy: (calendarSuggestion: SuggestionHolders, accountConfigId: string, suggestions: CalendarSuggestionItem[]) => Promise<{
10
+ suggestion: CalendarSuggestionItem;
11
+ answerOvertakenBy: CalendarAnswerOvertakenValue;
12
+ }[]>;
13
+ export declare const reopenCalendarSuggestion: (store: ICalendarStore, input: ReopenCalendarSuggestionInput) => Promise<CalendarResult<CalendarSuggestionItem>>;
14
+ export {};
package/dist/reopen.js ADDED
@@ -0,0 +1,92 @@
1
+ import { CalendarAnswerOvertaken, CalendarInviteMethod, CalendarSuggestionState, } from "@remit/domain-enums";
2
+ import { calendarFailure } from "./errors.js";
3
+ import { deleteCalendarObjectWrite } from "./put.js";
4
+ import { followingUidPrefix } from "./scope.js";
5
+ const answeredStates = new Set([
6
+ CalendarSuggestionState.Accepted,
7
+ CalendarSuggestionState.Tentative,
8
+ ]);
9
+ const holdersByObject = async (calendarSuggestion, accountConfigId, suggestions) => {
10
+ const objectIds = [
11
+ ...new Set(suggestions
12
+ .map((suggestion) => suggestion.acceptedCalendarObjectId)
13
+ .filter((objectId) => objectId !== "")),
14
+ ];
15
+ const byObject = new Map();
16
+ for (const holder of await calendarSuggestion.listByAcceptedCalendarObjects(accountConfigId, objectIds)) {
17
+ if (!answeredStates.has(holder.state))
18
+ continue;
19
+ const held = byObject.get(holder.acceptedCalendarObjectId) ?? [];
20
+ held.push(holder);
21
+ byObject.set(holder.acceptedCalendarObjectId, held);
22
+ }
23
+ return byObject;
24
+ };
25
+ const othersIn = (byObject, suggestion) => (byObject.get(suggestion.acceptedCalendarObjectId) ?? []).filter((other) => other.suggestionId !== suggestion.suggestionId);
26
+ const overtakenOf = (holders) => holders.some((other) => other.method === CalendarInviteMethod.Cancel)
27
+ ? CalendarAnswerOvertaken.Cancellation
28
+ : CalendarAnswerOvertaken.None;
29
+ export const answersOvertakenBy = async (calendarSuggestion, accountConfigId, suggestions) => {
30
+ const byObject = await holdersByObject(calendarSuggestion, accountConfigId, suggestions);
31
+ return suggestions.map((suggestion) => ({
32
+ suggestion,
33
+ answerOvertakenBy: overtakenOf(othersIn(byObject, suggestion)),
34
+ }));
35
+ };
36
+ const locateAnswered = async (repos, collections, calendarObjectId) => {
37
+ for (const collection of collections) {
38
+ const object = await repos.calendarObject.find(collection.calendarId, calendarObjectId);
39
+ if (object !== null)
40
+ return { collection, object };
41
+ }
42
+ return undefined;
43
+ };
44
+ const partsOf = async (repos, suggestion, collection, answered) => [
45
+ answered,
46
+ ...(await repos.calendarObject.listByUidPrefix(collection.calendarId, followingUidPrefix(suggestion.icalUid))),
47
+ ];
48
+ export const reopenCalendarSuggestion = async (store, input) => {
49
+ const { accountConfigId, suggestion } = input;
50
+ if (suggestion.state === CalendarSuggestionState.Pending) {
51
+ return { ok: true, value: suggestion };
52
+ }
53
+ if (suggestion.state === CalendarSuggestionState.Superseded) {
54
+ return calendarFailure("NotReopenable", "A later message replaced this invitation, so answer that one instead.");
55
+ }
56
+ if (suggestion.method === CalendarInviteMethod.Cancel) {
57
+ return calendarFailure("NotReopenable", "This is a cancellation, so there is no answer to take back.");
58
+ }
59
+ const { repos } = store;
60
+ const holders = othersIn(await holdersByObject(repos.calendarSuggestion, accountConfigId, [
61
+ suggestion,
62
+ ]), suggestion);
63
+ if (overtakenOf(holders) === CalendarAnswerOvertaken.Cancellation) {
64
+ return calendarFailure("AnswerOvertaken", "The cancellation of this event was answered into the same event, so this answer can no longer be taken back.");
65
+ }
66
+ const located = suggestion.acceptedCalendarObjectId === "" || holders.length > 0
67
+ ? undefined
68
+ : await locateAnswered(repos, await repos.calendarCollection.listByAccountConfig(accountConfigId), suggestion.acceptedCalendarObjectId);
69
+ const removals = located
70
+ ? (await partsOf(repos, suggestion, located.collection, located.object)).map((object) => deleteCalendarObjectWrite({
71
+ accountConfigId,
72
+ calendarId: located.collection.calendarId,
73
+ calendarObjectId: object.calendarObjectId,
74
+ }))
75
+ : [];
76
+ await store.commit([
77
+ ...removals,
78
+ {
79
+ kind: "SettleSuggestion",
80
+ accountConfigId,
81
+ suggestionId: suggestion.suggestionId,
82
+ settle: {
83
+ state: CalendarSuggestionState.Pending,
84
+ acceptedCalendarObjectId: "",
85
+ },
86
+ },
87
+ ]);
88
+ return {
89
+ ok: true,
90
+ value: await repos.calendarSuggestion.get(accountConfigId, suggestion.suggestionId),
91
+ };
92
+ };
@@ -0,0 +1,70 @@
1
+ import { RecurrenceScope } from "@remit/domain-enums";
2
+ import ICAL from "ical.js";
3
+ import { type CalendarEventFields } from "./build.js";
4
+ import { type CalendarResult } from "./errors.js";
5
+ import { type ParsedCalendar } from "./parse.js";
6
+ export type RecurrenceScopeValue = (typeof RecurrenceScope)[keyof typeof RecurrenceScope];
7
+ /**
8
+ * What a scoped write turns into on the store.
9
+ *
10
+ * `Split` is the only one that is not a single resource: iCalendar has no way
11
+ * to say "the rest of this series is different", so a `Following` edit really
12
+ * is two resources — the truncated original and a new one under a UID of its
13
+ * own. Both go through the one write path rather than through a second.
14
+ */
15
+ export type ScopedWrite = {
16
+ kind: "Replace";
17
+ icalData: string;
18
+ } | {
19
+ kind: "Split";
20
+ icalData: string;
21
+ following: string;
22
+ } | {
23
+ kind: "Delete";
24
+ };
25
+ export interface ScopedWriteInput {
26
+ scope: RecurrenceScopeValue;
27
+ /** ISO 8601 UTC instant naming the occurrence the write was made from, or `""`. */
28
+ recurrenceId: string;
29
+ /** UID the resource a `Following` split creates is written under. */
30
+ followingUid: string;
31
+ }
32
+ export declare const followingUidPrefix: (icalUid: string) => string;
33
+ export declare const followingUidOf: (icalUid: string, id: string) => string;
34
+ interface FoundOccurrence {
35
+ /** The rule slot, in whatever form the master's DTSTART is written in. */
36
+ slot: ICAL.Time;
37
+ slotUtc: string;
38
+ /** The occurrence as the series produces it; absent when only an override names the slot. */
39
+ details: ReturnType<ICAL.Event["getOccurrenceDetails"]> | null;
40
+ /** Position in the rule's own sequence, `-1` when the rule never reaches it. */
41
+ index: number;
42
+ override: ICAL.Component | null;
43
+ }
44
+ /**
45
+ * The occurrence a scoped write is anchored at.
46
+ *
47
+ * Walks the series rather than trusting the caller's instant: a RECURRENCE-ID
48
+ * naming no occurrence is the shape of a stale client acting on a series
49
+ * somebody has since edited, and writing an override or an EXDATE for a slot
50
+ * the rule never produces leaves a resource nothing can reconcile afterwards.
51
+ */
52
+ export declare const findOccurrence: (calendar: ParsedCalendar, collectionTimezone: string, recurrenceId: string) => CalendarResult<FoundOccurrence>;
53
+ /**
54
+ * Turns an edit of one drawing of a series into the resource writes it means.
55
+ *
56
+ * `All` rewrites the master, moved from the occurrence it names when it names
57
+ * one. `This` writes a RECURRENCE-ID override, which is
58
+ * the only thing iCalendar has for "this one is different". `Following` splits,
59
+ * because a rule cannot change halfway through.
60
+ */
61
+ export declare const applyScopedUpdate: (calendar: ParsedCalendar, collectionTimezone: string, input: ScopedWriteInput, patch: Partial<CalendarEventFields>) => Promise<CalendarResult<ScopedWrite>>;
62
+ /**
63
+ * Turns a delete of one drawing of a series into the resource writes it means.
64
+ *
65
+ * `This` is an EXDATE and `Following` truncates the rule; neither removes the
66
+ * resource, because the rest of the series is still somebody's calendar. `All`
67
+ * removes it.
68
+ */
69
+ export declare const applyScopedDelete: (calendar: ParsedCalendar, collectionTimezone: string, input: ScopedWriteInput) => Promise<CalendarResult<ScopedWrite>>;
70
+ export {};