@remit/calendar-service 0.0.21 → 1.0.1
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/dist/accept.d.ts +2 -0
- package/dist/accept.js +6 -7
- package/dist/errors.d.ts +13 -1
- package/dist/index.d.ts +7 -2
- package/dist/index.js +7 -2
- package/dist/invitations.d.ts +120 -0
- package/dist/invitations.js +321 -0
- package/dist/memory-store.d.ts +3 -3
- package/dist/memory-store.js +15 -9
- package/dist/participation.d.ts +36 -0
- package/dist/participation.js +104 -0
- package/dist/put.d.ts +7 -0
- package/dist/put.js +14 -1
- package/dist/reply.d.ts +30 -0
- package/dist/reply.js +154 -0
- package/dist/scheduling.d.ts +67 -0
- package/dist/scheduling.js +208 -0
- package/dist/task.d.ts +33 -0
- package/dist/task.js +153 -0
- package/dist/window.d.ts +8 -5
- package/dist/window.js +39 -33
- package/package.json +1 -1
package/dist/accept.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CalendarInviteAnswer as CalendarInviteAnswerValue } from "@remit/api-openapi-types";
|
|
2
2
|
import type { CalendarObjectItem, CalendarSuggestionItem, ICalendarStore } from "@remit/data-ports";
|
|
3
3
|
import type { CalendarResult } from "./errors.js";
|
|
4
|
+
/** The state a suggestion settles in for each answer. */
|
|
5
|
+
export declare const SUGGESTION_STATE_OF_ANSWER: Record<CalendarInviteAnswerValue, CalendarSuggestionItem["state"]>;
|
|
4
6
|
/**
|
|
5
7
|
* The VCALENDAR a stored resource is made of, built from the invitation's own
|
|
6
8
|
* bytes.
|
package/dist/accept.js
CHANGED
|
@@ -2,16 +2,15 @@ import { deriveCalendarObjectId } from "@remit/data-ports/id";
|
|
|
2
2
|
import { CalendarInviteAnswer, CalendarInviteMethod, CalendarSuggestionState, } from "@remit/domain-enums";
|
|
3
3
|
import ICAL from "ical.js";
|
|
4
4
|
import { parseCalendar, serializeCalendar } from "./parse.js";
|
|
5
|
+
import { PARTSTAT_OF_ANSWER } from "./participation.js";
|
|
5
6
|
import { prepareCalendarObject, readWrittenObject } from "./put.js";
|
|
6
7
|
import { mailAddressOf } from "./suggest.js";
|
|
7
8
|
const eventsOf = (component) => component.getAllSubcomponents("vevent");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
[CalendarInviteAnswer.Tentative]: "TENTATIVE",
|
|
11
|
-
};
|
|
12
|
-
const stateOf = {
|
|
9
|
+
/** The state a suggestion settles in for each answer. */
|
|
10
|
+
export const SUGGESTION_STATE_OF_ANSWER = {
|
|
13
11
|
[CalendarInviteAnswer.Accepted]: CalendarSuggestionState.Accepted,
|
|
14
12
|
[CalendarInviteAnswer.Tentative]: CalendarSuggestionState.Tentative,
|
|
13
|
+
[CalendarInviteAnswer.Declined]: CalendarSuggestionState.Declined,
|
|
15
14
|
};
|
|
16
15
|
/**
|
|
17
16
|
* Marks one address with its answer, on every VEVENT of the resource — the
|
|
@@ -29,7 +28,7 @@ const markAnswered = (component, attendee, answer) => {
|
|
|
29
28
|
.find((property) => mailAddressOf(String(property.getFirstValue())).toLowerCase() ===
|
|
30
29
|
wanted);
|
|
31
30
|
const property = existing ?? new ICAL.Property("attendee", event);
|
|
32
|
-
property.setParameter("partstat",
|
|
31
|
+
property.setParameter("partstat", PARTSTAT_OF_ANSWER[answer]);
|
|
33
32
|
if (!existing) {
|
|
34
33
|
property.setValue(`mailto:${attendee}`);
|
|
35
34
|
event.addProperty(property);
|
|
@@ -143,7 +142,7 @@ export const acceptCalendarSuggestion = async (store, input) => {
|
|
|
143
142
|
accountConfigId: input.accountConfigId,
|
|
144
143
|
suggestionId: input.suggestion.suggestionId,
|
|
145
144
|
settle: {
|
|
146
|
-
state:
|
|
145
|
+
state: SUGGESTION_STATE_OF_ANSWER[input.answer],
|
|
147
146
|
acceptedCalendarObjectId: calendarObjectId,
|
|
148
147
|
},
|
|
149
148
|
},
|
package/dist/errors.d.ts
CHANGED
|
@@ -36,7 +36,19 @@ export type CalendarValidationCode =
|
|
|
36
36
|
/** A RECURRENCE-ID naming no occurrence this series produces. */
|
|
37
37
|
| "UnknownOccurrence" | "UnmovableRecurrenceRule" | "UneditableCancellation"
|
|
38
38
|
/** A subscription address that is not an http, https or webcal URL. */
|
|
39
|
-
| "InvalidSubscriptionUrl" | "NotReopenable" | "AnswerOvertaken"
|
|
39
|
+
| "InvalidSubscriptionUrl" | "NotReopenable" | "AnswerOvertaken"
|
|
40
|
+
/** An answer to the rest of a series, which is not a thing an answer can be to. */
|
|
41
|
+
| "InvalidScope"
|
|
42
|
+
/** A guest list change on an event somebody else organizes. */
|
|
43
|
+
| "NotOrganizer"
|
|
44
|
+
/** An answer from somebody who is not on the guest list. */
|
|
45
|
+
| "NotAGuest"
|
|
46
|
+
/** A guest that is not a mail address. */
|
|
47
|
+
| "InvalidGuest"
|
|
48
|
+
/** A sending account that is not the caller's. */
|
|
49
|
+
| "UnknownOrganizer"
|
|
50
|
+
/** A sending account with Mail turned off, or no account to send from. */
|
|
51
|
+
| "OrganizerCannotSend";
|
|
40
52
|
export interface CalendarValidationError {
|
|
41
53
|
code: CalendarValidationCode;
|
|
42
54
|
message: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
|
-
export { type AcceptCalendarSuggestionInput, type AcceptedCalendarSuggestion, acceptCalendarSuggestion, buildAcceptedCalendar, } from "./accept.js";
|
|
1
|
+
export { type AcceptCalendarSuggestionInput, type AcceptedCalendarSuggestion, acceptCalendarSuggestion, buildAcceptedCalendar, SUGGESTION_STATE_OF_ANSWER, } from "./accept.js";
|
|
2
2
|
export { applyEventFields, buildEventCalendar, CALENDAR_PRODID, type CalendarEventFields, eventTimeFields, OFFSET_BEARING, readEventTime, readRecurrenceRule, } from "./build.js";
|
|
3
3
|
export { correctCalendarSuggestion } from "./correct.js";
|
|
4
4
|
export type { CalendarResult, CalendarValidationCode, CalendarValidationError, } from "./errors.js";
|
|
5
5
|
export { computeEtag } from "./etag.js";
|
|
6
6
|
export { CALENDAR_EXPANSION_HORIZON_DAYS, CALENDAR_EXPANSION_MAX_OCCURRENCES, CALENDAR_WINDOW_MAX_STEPS, type CalendarExpansion, expandCalendar, expandCalendarWindow, } from "./expand.js";
|
|
7
7
|
export { buildCalendarFeed, CALENDAR_FEED_PATH_PREFIX, CALENDAR_FEED_PATH_SUFFIX, CALENDAR_FEED_TOKEN_BYTES, type CalendarFeed, type CalendarFeedSecret, calendarFeedIsUnchanged, calendarFeedIsUnmodifiedSince, hashCalendarFeedToken, isCalendarFeedToken, mintCalendarFeedToken, readCalendarFeedToken, redactCalendarFeedPath, } from "./feed.js";
|
|
8
|
+
export { type EventSchedulingUpdate, guestListOf, invitationSenderInEffect, type PlannedEventUpdate, planCreateSends, planDeleteFanOut, planDeleteSends, planEventCreate, planEventUpdate, planGuestFanOut, planUpdateSends, type SchedulingSendPlan, type SchedulingSends, schedulingBody, sendingAccountFor, sendsMail, shapeScheduledUpdate, } from "./invitations.js";
|
|
8
9
|
export { type ParsedCalendar, parseCalendar, serializeCalendar, } from "./parse.js";
|
|
10
|
+
export { attendeesOf, calendarAddressOf, countAttendees, organizerOf, PARTSTAT_OF_ANSWER, participationAtSlot, participationOf, selfAddressSet, } from "./participation.js";
|
|
9
11
|
export { type CalendarObjectProjection, projectCalendar, } from "./project.js";
|
|
10
|
-
export { DEFAULT_CALENDAR_URL_SEGMENT, deleteCalendarObject, deleteCalendarObjectWrite, type PutCalendarObjectInput, prepareCalendarObject, provisionDefaultCalendar, putCalendarObject, readWrittenObject, } from "./put.js";
|
|
12
|
+
export { DEFAULT_CALENDAR_URL_SEGMENT, deleteCalendarObject, deleteCalendarObjectWrite, type PutCalendarObjectInput, prepareCalendarObject, provisionDefaultCalendar, provisionTaskCalendar, putCalendarObject, readWrittenObject, TASK_CALENDAR_URL_SEGMENT, } from "./put.js";
|
|
11
13
|
export { answersOvertakenBy, type ReopenCalendarSuggestionInput, reopenCalendarSuggestion, } from "./reopen.js";
|
|
14
|
+
export { type RecordCalendarReplyInput, type RecordCalendarReplyOutcome, type ReplyRevision, recordCalendarReply, replyOvertakes, } from "./reply.js";
|
|
15
|
+
export { applyAnswer, applyGuestList, buildSchedulingMessage, bumpSequence, type CalendarPerson, distinctGuests, guestAddressesOf, invalidGuestOf, type SchedulingMessage, type SchedulingMessageInput, type SchedulingMethod, schedulingSubject, } from "./scheduling.js";
|
|
12
16
|
export { applyScopedDelete, applyScopedUpdate, findOccurrence, followingUidOf, followingUidPrefix, type RecurrenceScopeValue, type ScopedWrite, type ScopedWriteInput, } from "./scope.js";
|
|
13
17
|
export { CALENDAR_SUBSCRIPTION_MAX_BYTES, CALENDAR_SUBSCRIPTION_TIMEOUT_MS, type CalendarFeedFetch, type CalendarFeedFetcher, type CalendarFeedResource, type CalendarFeedSplit, type CalendarFeedWrite, type CalendarSubscriptionRefresh, type CalendarSubscriptionTarget, fetchCalendarFeed, isSubscriptionDue, readCalendarFeed, readSubscriptionUrl, recordCalendarFeedFailure, refreshCalendarSubscription, splitCalendarFeed, writeCalendarFeed, } from "./subscribe.js";
|
|
14
18
|
export { type CalendarSuggestionProjection, inviteMethodOf, mailAddressOf, projectSuggestion, type RecordCalendarSuggestionInput, type RecordedCalendarSuggestion, recordCalendarSuggestion, } from "./suggest.js";
|
|
19
|
+
export { buildTaskCalendar, type CalendarTaskFields, listTasksInWindow, parseTaskCalendar, prepareCalendarTask, updateTaskCalendar, } from "./task.js";
|
|
15
20
|
export { civilInZone, isResolvableZone, type ResolvedTime, resolveTime, toOffsetIso, toUtcIso, } from "./time.js";
|
|
16
21
|
export { type BusySpan, CALENDAR_WINDOW_LOOKBACK_DAYS, type CalendarInstance, type CalendarWindow, type CalendarWindowRepositories, isBusy, listBusySpans, listCalendarInstances, listObjectInstances, mergeBusySpans, } from "./window.js";
|
package/dist/index.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
export { acceptCalendarSuggestion, buildAcceptedCalendar, } from "./accept.js";
|
|
1
|
+
export { acceptCalendarSuggestion, buildAcceptedCalendar, SUGGESTION_STATE_OF_ANSWER, } from "./accept.js";
|
|
2
2
|
export { applyEventFields, buildEventCalendar, CALENDAR_PRODID, eventTimeFields, OFFSET_BEARING, readEventTime, readRecurrenceRule, } from "./build.js";
|
|
3
3
|
export { correctCalendarSuggestion } from "./correct.js";
|
|
4
4
|
export { computeEtag } from "./etag.js";
|
|
5
5
|
export { CALENDAR_EXPANSION_HORIZON_DAYS, CALENDAR_EXPANSION_MAX_OCCURRENCES, CALENDAR_WINDOW_MAX_STEPS, expandCalendar, expandCalendarWindow, } from "./expand.js";
|
|
6
6
|
export { buildCalendarFeed, CALENDAR_FEED_PATH_PREFIX, CALENDAR_FEED_PATH_SUFFIX, CALENDAR_FEED_TOKEN_BYTES, calendarFeedIsUnchanged, calendarFeedIsUnmodifiedSince, hashCalendarFeedToken, isCalendarFeedToken, mintCalendarFeedToken, readCalendarFeedToken, redactCalendarFeedPath, } from "./feed.js";
|
|
7
|
+
export { guestListOf, invitationSenderInEffect, planCreateSends, planDeleteFanOut, planDeleteSends, planEventCreate, planEventUpdate, planGuestFanOut, planUpdateSends, schedulingBody, sendingAccountFor, sendsMail, shapeScheduledUpdate, } from "./invitations.js";
|
|
7
8
|
export { parseCalendar, serializeCalendar, } from "./parse.js";
|
|
9
|
+
export { attendeesOf, calendarAddressOf, countAttendees, organizerOf, PARTSTAT_OF_ANSWER, participationAtSlot, participationOf, selfAddressSet, } from "./participation.js";
|
|
8
10
|
export { projectCalendar, } from "./project.js";
|
|
9
|
-
export { DEFAULT_CALENDAR_URL_SEGMENT, deleteCalendarObject, deleteCalendarObjectWrite, prepareCalendarObject, provisionDefaultCalendar, putCalendarObject, readWrittenObject, } from "./put.js";
|
|
11
|
+
export { DEFAULT_CALENDAR_URL_SEGMENT, deleteCalendarObject, deleteCalendarObjectWrite, prepareCalendarObject, provisionDefaultCalendar, provisionTaskCalendar, putCalendarObject, readWrittenObject, TASK_CALENDAR_URL_SEGMENT, } from "./put.js";
|
|
10
12
|
export { answersOvertakenBy, reopenCalendarSuggestion, } from "./reopen.js";
|
|
13
|
+
export { recordCalendarReply, replyOvertakes, } from "./reply.js";
|
|
14
|
+
export { applyAnswer, applyGuestList, buildSchedulingMessage, bumpSequence, distinctGuests, guestAddressesOf, invalidGuestOf, schedulingSubject, } from "./scheduling.js";
|
|
11
15
|
export { applyScopedDelete, applyScopedUpdate, findOccurrence, followingUidOf, followingUidPrefix, } from "./scope.js";
|
|
12
16
|
export { CALENDAR_SUBSCRIPTION_MAX_BYTES, CALENDAR_SUBSCRIPTION_TIMEOUT_MS, fetchCalendarFeed, isSubscriptionDue, readCalendarFeed, readSubscriptionUrl, recordCalendarFeedFailure, refreshCalendarSubscription, splitCalendarFeed, writeCalendarFeed, } from "./subscribe.js";
|
|
13
17
|
export { inviteMethodOf, mailAddressOf, projectSuggestion, recordCalendarSuggestion, } from "./suggest.js";
|
|
18
|
+
export { buildTaskCalendar, listTasksInWindow, parseTaskCalendar, prepareCalendarTask, updateTaskCalendar, } from "./task.js";
|
|
14
19
|
export { civilInZone, isResolvableZone, resolveTime, toOffsetIso, toUtcIso, } from "./time.js";
|
|
15
20
|
export { CALENDAR_WINDOW_LOOKBACK_DAYS, isBusy, listBusySpans, listCalendarInstances, listObjectInstances, mergeBusySpans, } from "./window.js";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { CalendarInviteAnswer as CalendarInviteAnswerValue } from "@remit/api-openapi-types";
|
|
2
|
+
import type { AccountItem } from "@remit/data-ports";
|
|
3
|
+
import type { CalendarEventFields } from "./build.js";
|
|
4
|
+
import { type CalendarResult } from "./errors.js";
|
|
5
|
+
import { type ParsedCalendar } from "./parse.js";
|
|
6
|
+
import { type CalendarPerson, type SchedulingMethod } from "./scheduling.js";
|
|
7
|
+
import type { RecurrenceScopeValue, ScopedWrite } from "./scope.js";
|
|
8
|
+
type Account = Pick<AccountItem, "accountId" | "email" | "syncedServices">;
|
|
9
|
+
/** Whether an account can send: a scheduling message goes out as mail. */
|
|
10
|
+
export declare const sendsMail: (account: Pick<AccountItem, "syncedServices">) => boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The account invitations go out from by default: the one the reader set, while
|
|
13
|
+
* it still sends mail, and otherwise the account an event is on. A calendar
|
|
14
|
+
* belongs to the account config rather than to one account, so a new event is
|
|
15
|
+
* on the config's first account that sends mail.
|
|
16
|
+
*/
|
|
17
|
+
export declare const invitationSenderInEffect: <A extends Account>(accounts: readonly A[], settingAccountId: string | undefined) => A | undefined;
|
|
18
|
+
/** The account a request names to send from, refused when it cannot send. */
|
|
19
|
+
export declare const sendingAccountFor: <A extends Account>(accounts: readonly A[], accountId: string) => CalendarResult<A>;
|
|
20
|
+
/** A guest list as the editor sent it, refused when an entry is not an address. */
|
|
21
|
+
export declare const guestListOf: (organizer: string, attendees: readonly CalendarPerson[]) => CalendarResult<CalendarPerson[]>;
|
|
22
|
+
/**
|
|
23
|
+
* Who gets which scheduling message after an organizer saves an event.
|
|
24
|
+
*
|
|
25
|
+
* Every guest still on the list gets the event as it now stands: a save is a
|
|
26
|
+
* revision, and a guest who keeps the old one keeps the old time. A guest the
|
|
27
|
+
* save took off the list gets a cancellation, and only them.
|
|
28
|
+
*/
|
|
29
|
+
export declare const planGuestFanOut: (previousGuests: readonly string[], currentGuests: readonly string[]) => {
|
|
30
|
+
request: string[];
|
|
31
|
+
cancel: string[];
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* What a delete sends. Removing the whole event cancels it for every guest.
|
|
35
|
+
* Removing one occurrence or the rest of a series leaves an event behind, and
|
|
36
|
+
* the guests get that event — with the EXDATE or the shortened rule — rather
|
|
37
|
+
* than a cancellation of a series that still exists.
|
|
38
|
+
*/
|
|
39
|
+
export declare const planDeleteFanOut: (removedWhole: boolean, guests: readonly string[]) => {
|
|
40
|
+
method: SchedulingMethod;
|
|
41
|
+
recipients: string[];
|
|
42
|
+
};
|
|
43
|
+
/** One message to queue: about which stored resource, to whom, saying what. */
|
|
44
|
+
export interface SchedulingSendPlan {
|
|
45
|
+
method: SchedulingMethod;
|
|
46
|
+
calendarObjectId: string;
|
|
47
|
+
/** The stored resource the message is built from. */
|
|
48
|
+
icalData: string;
|
|
49
|
+
collectionTimezone: string;
|
|
50
|
+
recipient: CalendarPerson;
|
|
51
|
+
/** For a REPLY: the guest answering. For the rest: the recipient. */
|
|
52
|
+
attendee: CalendarPerson;
|
|
53
|
+
recurrenceId: string;
|
|
54
|
+
answer?: CalendarInviteAnswerValue;
|
|
55
|
+
}
|
|
56
|
+
/** The messages a write queues, and the account they go out from. */
|
|
57
|
+
export interface SchedulingSends<A> {
|
|
58
|
+
from: A;
|
|
59
|
+
plans: SchedulingSendPlan[];
|
|
60
|
+
}
|
|
61
|
+
/** The plain-text body a scheduling message carries beside its calendar part. */
|
|
62
|
+
export declare const schedulingBody: (subject: string, icalData: string) => string;
|
|
63
|
+
/** What a create must know before it writes: who sends, and to whom. */
|
|
64
|
+
export declare const planEventCreate: <A extends Account>(accounts: readonly A[], settingAccountId: string | undefined, attendees: readonly CalendarPerson[], organizerAccountId: string | undefined) => CalendarResult<{
|
|
65
|
+
organizer: A;
|
|
66
|
+
guests: CalendarPerson[];
|
|
67
|
+
} | null>;
|
|
68
|
+
export declare const planCreateSends: <A extends Account>(planned: {
|
|
69
|
+
organizer: A;
|
|
70
|
+
guests: CalendarPerson[];
|
|
71
|
+
}, stored: {
|
|
72
|
+
calendarObjectId: string;
|
|
73
|
+
icalData: string;
|
|
74
|
+
}, collectionTimezone: string) => SchedulingSends<A>;
|
|
75
|
+
/** The scheduling half of an event update. */
|
|
76
|
+
export interface EventSchedulingUpdate {
|
|
77
|
+
attendees?: CalendarPerson[];
|
|
78
|
+
organizerAccountId?: string;
|
|
79
|
+
answer?: CalendarInviteAnswerValue;
|
|
80
|
+
}
|
|
81
|
+
/** What an update decided about scheduling before anything is written. */
|
|
82
|
+
export interface PlannedEventUpdate<A> {
|
|
83
|
+
storedOrganizer: string;
|
|
84
|
+
previousGuests: CalendarPerson[];
|
|
85
|
+
guests: CalendarPerson[];
|
|
86
|
+
/** The account the organizer's messages go out from; absent when none are owed. */
|
|
87
|
+
organizer: A | undefined;
|
|
88
|
+
/** The reader's own address on the guest list, when they answer. */
|
|
89
|
+
answeringAs: string;
|
|
90
|
+
/** Whether the organizer's guests are owed the revision. */
|
|
91
|
+
organized: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Decides who an update writes as and owes messages to, refused before
|
|
95
|
+
* anything is written: a guest list only its organizer may change, an answer
|
|
96
|
+
* only a guest may give, and nobody to send from refuses the save rather than
|
|
97
|
+
* leaving guests uninvited.
|
|
98
|
+
*/
|
|
99
|
+
export declare const planEventUpdate: <A extends Account>(calendar: ParsedCalendar, accounts: readonly A[], settingAccountId: string | undefined, scope: RecurrenceScopeValue, update: EventSchedulingUpdate) => CalendarResult<PlannedEventUpdate<A>>;
|
|
100
|
+
/**
|
|
101
|
+
* The bytes an update stores: the guest list on the resource the patch applied
|
|
102
|
+
* to, SEQUENCE moved past the last revision when the organizer reschedules, and
|
|
103
|
+
* the reader's own answer on the occurrence or the series they answered.
|
|
104
|
+
*/
|
|
105
|
+
export declare const shapeScheduledUpdate: <A extends Account>(write: Exclude<ScopedWrite, {
|
|
106
|
+
kind: "Delete";
|
|
107
|
+
}>, planned: PlannedEventUpdate<A>, update: EventSchedulingUpdate, patch: Partial<CalendarEventFields>, collectionTimezone: string, scope: RecurrenceScopeValue, recurrenceId: string) => Promise<CalendarResult<ScopedWrite>>;
|
|
108
|
+
interface StoredResource {
|
|
109
|
+
calendarObjectId: string;
|
|
110
|
+
icalData: string;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The messages an update owes: the revision to every guest of each resource
|
|
114
|
+
* it wrote, a cancellation to each guest it removed, and the reader's reply to
|
|
115
|
+
* the organizer when they answered as a guest.
|
|
116
|
+
*/
|
|
117
|
+
export declare const planUpdateSends: <A extends Account>(planned: PlannedEventUpdate<A>, accounts: readonly A[], update: EventSchedulingUpdate, original: StoredResource, written: readonly StoredResource[], collectionTimezone: string, scope: RecurrenceScopeValue, recurrenceId: string) => Promise<SchedulingSends<A> | null>;
|
|
118
|
+
/** The messages a delete owes, only on an event the reader organizes. */
|
|
119
|
+
export declare const planDeleteSends: <A extends Account>(calendar: ParsedCalendar, accounts: readonly A[], original: StoredResource, remaining: StoredResource | null, collectionTimezone: string) => SchedulingSends<A> | null;
|
|
120
|
+
export {};
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { AccountService, CalendarInviteMethod, CalendarParticipation, RecurrenceScope, } from "@remit/domain-enums";
|
|
2
|
+
import { calendarFailure } from "./errors.js";
|
|
3
|
+
import { parseCalendar } from "./parse.js";
|
|
4
|
+
import { attendeesOf, calendarAddressOf, organizerOf, participationOf, selfAddressSet, } from "./participation.js";
|
|
5
|
+
import { applyAnswer, applyGuestList, bumpSequence, distinctGuests, guestAddressesOf, invalidGuestOf, } from "./scheduling.js";
|
|
6
|
+
/** Whether an account can send: a scheduling message goes out as mail. */
|
|
7
|
+
export const sendsMail = (account) => account.syncedServices.includes(AccountService.Mail);
|
|
8
|
+
/**
|
|
9
|
+
* The account invitations go out from by default: the one the reader set, while
|
|
10
|
+
* it still sends mail, and otherwise the account an event is on. A calendar
|
|
11
|
+
* belongs to the account config rather than to one account, so a new event is
|
|
12
|
+
* on the config's first account that sends mail.
|
|
13
|
+
*/
|
|
14
|
+
export const invitationSenderInEffect = (accounts, settingAccountId) => accounts.find((account) => account.accountId === settingAccountId && sendsMail(account)) ?? accounts.find(sendsMail);
|
|
15
|
+
/** The account a request names to send from, refused when it cannot send. */
|
|
16
|
+
export const sendingAccountFor = (accounts, accountId) => {
|
|
17
|
+
const account = accounts.find((candidate) => candidate.accountId === accountId);
|
|
18
|
+
if (!account) {
|
|
19
|
+
return calendarFailure("UnknownOrganizer", `no account ${accountId} on this account config to send the invitations from`);
|
|
20
|
+
}
|
|
21
|
+
if (!sendsMail(account)) {
|
|
22
|
+
return calendarFailure("OrganizerCannotSend", `${account.email} has Mail turned off, so it cannot send invitations. Turn Mail on for it in Settings, or send from another account.`);
|
|
23
|
+
}
|
|
24
|
+
return { ok: true, value: account };
|
|
25
|
+
};
|
|
26
|
+
const accountByAddress = (accounts, address) => accounts.find((account) => account.email.toLowerCase() === address);
|
|
27
|
+
/** A guest list as the editor sent it, refused when an entry is not an address. */
|
|
28
|
+
export const guestListOf = (organizer, attendees) => {
|
|
29
|
+
const invalid = invalidGuestOf(attendees);
|
|
30
|
+
if (invalid) {
|
|
31
|
+
return calendarFailure("InvalidGuest", `"${invalid.email}" is not a mail address, so no invitation can go to it`);
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, value: distinctGuests(organizer, attendees) };
|
|
34
|
+
};
|
|
35
|
+
const guestPeopleOf = (calendar) => attendeesOf(calendar.master)
|
|
36
|
+
.filter((attendee) => attendee.role === "Guest")
|
|
37
|
+
.map((attendee) => ({
|
|
38
|
+
email: attendee.email,
|
|
39
|
+
displayName: attendee.displayName,
|
|
40
|
+
}));
|
|
41
|
+
const personOf = (people, email) => people.find((person) => person.email === email) ?? { email, displayName: "" };
|
|
42
|
+
/**
|
|
43
|
+
* Who gets which scheduling message after an organizer saves an event.
|
|
44
|
+
*
|
|
45
|
+
* Every guest still on the list gets the event as it now stands: a save is a
|
|
46
|
+
* revision, and a guest who keeps the old one keeps the old time. A guest the
|
|
47
|
+
* save took off the list gets a cancellation, and only them.
|
|
48
|
+
*/
|
|
49
|
+
export const planGuestFanOut = (previousGuests, currentGuests) => {
|
|
50
|
+
const current = new Set(currentGuests);
|
|
51
|
+
return {
|
|
52
|
+
request: [...current],
|
|
53
|
+
cancel: [...new Set(previousGuests)].filter((guest) => !current.has(guest)),
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* What a delete sends. Removing the whole event cancels it for every guest.
|
|
58
|
+
* Removing one occurrence or the rest of a series leaves an event behind, and
|
|
59
|
+
* the guests get that event — with the EXDATE or the shortened rule — rather
|
|
60
|
+
* than a cancellation of a series that still exists.
|
|
61
|
+
*/
|
|
62
|
+
export const planDeleteFanOut = (removedWhole, guests) => ({
|
|
63
|
+
method: removedWhole
|
|
64
|
+
? CalendarInviteMethod.Cancel
|
|
65
|
+
: CalendarInviteMethod.Request,
|
|
66
|
+
recipients: [...new Set(guests)],
|
|
67
|
+
});
|
|
68
|
+
/** The plain-text body a scheduling message carries beside its calendar part. */
|
|
69
|
+
export const schedulingBody = (subject, icalData) => {
|
|
70
|
+
const start = /\r?\nDTSTART[^:]*:([^\r\n]+)/.exec(icalData)?.[1] ?? "";
|
|
71
|
+
return start === "" ? `${subject}\n` : `${subject}\nWhen: ${start}\n`;
|
|
72
|
+
};
|
|
73
|
+
const requests = (guests, resource, collectionTimezone) => guests.map((guest) => ({
|
|
74
|
+
method: CalendarInviteMethod.Request,
|
|
75
|
+
calendarObjectId: resource.calendarObjectId,
|
|
76
|
+
icalData: resource.icalData,
|
|
77
|
+
collectionTimezone,
|
|
78
|
+
recipient: guest,
|
|
79
|
+
attendee: guest,
|
|
80
|
+
recurrenceId: "",
|
|
81
|
+
}));
|
|
82
|
+
/** What a create must know before it writes: who sends, and to whom. */
|
|
83
|
+
export const planEventCreate = (accounts, settingAccountId, attendees, organizerAccountId) => {
|
|
84
|
+
if (organizerAccountId !== undefined) {
|
|
85
|
+
const named = sendingAccountFor(accounts, organizerAccountId);
|
|
86
|
+
if (!named.ok)
|
|
87
|
+
return named;
|
|
88
|
+
}
|
|
89
|
+
if (attendees.length === 0)
|
|
90
|
+
return { ok: true, value: null };
|
|
91
|
+
const organizer = organizerAccountId !== undefined
|
|
92
|
+
? sendingAccountFor(accounts, organizerAccountId)
|
|
93
|
+
: (() => {
|
|
94
|
+
const inEffect = invitationSenderInEffect(accounts, settingAccountId);
|
|
95
|
+
return inEffect
|
|
96
|
+
? { ok: true, value: inEffect }
|
|
97
|
+
: calendarFailure("OrganizerCannotSend", "none of your accounts sends mail, so no invitation can go out");
|
|
98
|
+
})();
|
|
99
|
+
if (!organizer.ok)
|
|
100
|
+
return organizer;
|
|
101
|
+
const guests = guestListOf(organizer.value.email, attendees);
|
|
102
|
+
if (!guests.ok)
|
|
103
|
+
return guests;
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
value: { organizer: organizer.value, guests: guests.value },
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
export const planCreateSends = (planned, stored, collectionTimezone) => ({
|
|
110
|
+
from: planned.organizer,
|
|
111
|
+
plans: requests(planned.guests, stored, collectionTimezone),
|
|
112
|
+
});
|
|
113
|
+
/**
|
|
114
|
+
* Decides who an update writes as and owes messages to, refused before
|
|
115
|
+
* anything is written: a guest list only its organizer may change, an answer
|
|
116
|
+
* only a guest may give, and nobody to send from refuses the save rather than
|
|
117
|
+
* leaving guests uninvited.
|
|
118
|
+
*/
|
|
119
|
+
export const planEventUpdate = (calendar, accounts, settingAccountId, scope, update) => {
|
|
120
|
+
const self = selfAddressSet(accounts.map((account) => account.email));
|
|
121
|
+
const storedOrganizer = organizerOf(calendar.master);
|
|
122
|
+
const previousGuests = guestPeopleOf(calendar);
|
|
123
|
+
let answeringAs = "";
|
|
124
|
+
if (update.answer !== undefined) {
|
|
125
|
+
if (scope === RecurrenceScope.Following) {
|
|
126
|
+
return calendarFailure("InvalidScope", "an answer is to one occurrence (This) or to the series (All), never to the rest of it");
|
|
127
|
+
}
|
|
128
|
+
const participation = participationOf(calendar.master, self);
|
|
129
|
+
if (participation === CalendarParticipation.Organizer ||
|
|
130
|
+
participation === CalendarParticipation.Unscheduled) {
|
|
131
|
+
return calendarFailure("NotAGuest", "you are not a guest of this event, so there is no invitation to answer");
|
|
132
|
+
}
|
|
133
|
+
answeringAs =
|
|
134
|
+
calendar.master
|
|
135
|
+
.getAllProperties("attendee")
|
|
136
|
+
.map((line) => calendarAddressOf(line.getFirstValue()))
|
|
137
|
+
.find((address) => self.has(address)) ?? "";
|
|
138
|
+
}
|
|
139
|
+
if (update.organizerAccountId !== undefined) {
|
|
140
|
+
const named = sendingAccountFor(accounts, update.organizerAccountId);
|
|
141
|
+
if (!named.ok)
|
|
142
|
+
return named;
|
|
143
|
+
}
|
|
144
|
+
let organizer;
|
|
145
|
+
if (storedOrganizer !== "" && self.has(storedOrganizer)) {
|
|
146
|
+
organizer = accountByAddress(accounts, storedOrganizer);
|
|
147
|
+
}
|
|
148
|
+
if (update.attendees !== undefined) {
|
|
149
|
+
if (storedOrganizer !== "" && !organizer) {
|
|
150
|
+
return calendarFailure("NotOrganizer", `${storedOrganizer} organizes this event, so only they can change its guests`);
|
|
151
|
+
}
|
|
152
|
+
if (organizer &&
|
|
153
|
+
update.organizerAccountId !== undefined &&
|
|
154
|
+
update.organizerAccountId !== organizer.accountId) {
|
|
155
|
+
return calendarFailure("NotOrganizer", `${storedOrganizer} organizes this event; the organizer of an event never changes`);
|
|
156
|
+
}
|
|
157
|
+
if (!organizer) {
|
|
158
|
+
organizer =
|
|
159
|
+
update.organizerAccountId !== undefined
|
|
160
|
+
? accounts.find((account) => account.accountId === update.organizerAccountId)
|
|
161
|
+
: invitationSenderInEffect(accounts, settingAccountId);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (organizer &&
|
|
165
|
+
(update.attendees !== undefined || previousGuests.length > 0)) {
|
|
166
|
+
const sending = sendingAccountFor(accounts, organizer.accountId);
|
|
167
|
+
if (!sending.ok)
|
|
168
|
+
return sending;
|
|
169
|
+
}
|
|
170
|
+
if (update.attendees !== undefined && !organizer) {
|
|
171
|
+
return calendarFailure("OrganizerCannotSend", "none of your accounts sends mail, so no invitation can go out");
|
|
172
|
+
}
|
|
173
|
+
let guests = previousGuests;
|
|
174
|
+
if (update.attendees !== undefined && organizer) {
|
|
175
|
+
const listed = guestListOf(organizer.email, update.attendees);
|
|
176
|
+
if (!listed.ok)
|
|
177
|
+
return listed;
|
|
178
|
+
guests = listed.value;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
ok: true,
|
|
182
|
+
value: {
|
|
183
|
+
storedOrganizer,
|
|
184
|
+
previousGuests,
|
|
185
|
+
guests,
|
|
186
|
+
organizer,
|
|
187
|
+
answeringAs,
|
|
188
|
+
organized: organizer !== undefined && guests.length + previousGuests.length > 0,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
const touchesSchedule = (patch) => patch.start !== undefined ||
|
|
193
|
+
patch.end !== undefined ||
|
|
194
|
+
patch.allDay !== undefined ||
|
|
195
|
+
patch.timeZone !== undefined ||
|
|
196
|
+
patch.recurrenceRule !== undefined;
|
|
197
|
+
/**
|
|
198
|
+
* The bytes an update stores: the guest list on the resource the patch applied
|
|
199
|
+
* to, SEQUENCE moved past the last revision when the organizer reschedules, and
|
|
200
|
+
* the reader's own answer on the occurrence or the series they answered.
|
|
201
|
+
*/
|
|
202
|
+
export const shapeScheduledUpdate = async (write, planned, update, patch, collectionTimezone, scope, recurrenceId) => {
|
|
203
|
+
const reschedules = planned.organized && touchesSchedule(patch);
|
|
204
|
+
const finish = async (icalData, patched) => {
|
|
205
|
+
let next = icalData;
|
|
206
|
+
if (patched && planned.organizer && update.attendees !== undefined) {
|
|
207
|
+
next = await applyGuestList(next, { email: planned.organizer.email, displayName: "" }, planned.guests);
|
|
208
|
+
}
|
|
209
|
+
if (reschedules)
|
|
210
|
+
next = await bumpSequence(next);
|
|
211
|
+
return next;
|
|
212
|
+
};
|
|
213
|
+
if (write.kind === "Split") {
|
|
214
|
+
return {
|
|
215
|
+
ok: true,
|
|
216
|
+
value: {
|
|
217
|
+
kind: "Split",
|
|
218
|
+
icalData: await finish(write.icalData, false),
|
|
219
|
+
following: await finish(write.following, true),
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const replaced = await finish(write.icalData, true);
|
|
224
|
+
if (update.answer === undefined) {
|
|
225
|
+
return { ok: true, value: { kind: "Replace", icalData: replaced } };
|
|
226
|
+
}
|
|
227
|
+
const answered = await applyAnswer(replaced, collectionTimezone, planned.answeringAs, update.answer, scope === RecurrenceScope.This ? recurrenceId : "");
|
|
228
|
+
if (!answered.ok)
|
|
229
|
+
return answered;
|
|
230
|
+
return { ok: true, value: { kind: "Replace", icalData: answered.value } };
|
|
231
|
+
};
|
|
232
|
+
const guestsOfStored = async (icalData) => {
|
|
233
|
+
const parsed = await parseCalendar(icalData);
|
|
234
|
+
if (!parsed.ok) {
|
|
235
|
+
throw new Error(`a calendar resource this server wrote no longer parses: ${parsed.error.message}`);
|
|
236
|
+
}
|
|
237
|
+
return guestAddressesOf(parsed.value);
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* The messages an update owes: the revision to every guest of each resource
|
|
241
|
+
* it wrote, a cancellation to each guest it removed, and the reader's reply to
|
|
242
|
+
* the organizer when they answered as a guest.
|
|
243
|
+
*/
|
|
244
|
+
export const planUpdateSends = async (planned, accounts, update, original, written, collectionTimezone, scope, recurrenceId) => {
|
|
245
|
+
if (planned.organized && planned.organizer) {
|
|
246
|
+
const fanOut = planGuestFanOut(planned.previousGuests.map((guest) => guest.email), planned.guests.map((guest) => guest.email));
|
|
247
|
+
const plans = [];
|
|
248
|
+
for (const resource of written) {
|
|
249
|
+
const current = await guestsOfStored(resource.icalData);
|
|
250
|
+
plans.push(...requests(fanOut.request
|
|
251
|
+
.filter((email) => current.includes(email))
|
|
252
|
+
.map((email) => personOf(planned.guests, email)), resource, collectionTimezone));
|
|
253
|
+
}
|
|
254
|
+
for (const email of fanOut.cancel) {
|
|
255
|
+
const person = personOf(planned.previousGuests, email);
|
|
256
|
+
plans.push({
|
|
257
|
+
method: CalendarInviteMethod.Cancel,
|
|
258
|
+
calendarObjectId: original.calendarObjectId,
|
|
259
|
+
icalData: original.icalData,
|
|
260
|
+
collectionTimezone,
|
|
261
|
+
recipient: person,
|
|
262
|
+
attendee: person,
|
|
263
|
+
recurrenceId: "",
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return { from: planned.organizer, plans };
|
|
267
|
+
}
|
|
268
|
+
const result = written.at(-1);
|
|
269
|
+
if (update.answer === undefined ||
|
|
270
|
+
!result ||
|
|
271
|
+
planned.storedOrganizer === "") {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
const from = accountByAddress(accounts, planned.answeringAs);
|
|
275
|
+
if (!from) {
|
|
276
|
+
throw new Error(`${planned.answeringAs} answered as the reader but is none of their accounts`);
|
|
277
|
+
}
|
|
278
|
+
return {
|
|
279
|
+
from,
|
|
280
|
+
plans: [
|
|
281
|
+
{
|
|
282
|
+
method: CalendarInviteMethod.Reply,
|
|
283
|
+
calendarObjectId: result.calendarObjectId,
|
|
284
|
+
icalData: result.icalData,
|
|
285
|
+
collectionTimezone,
|
|
286
|
+
recipient: { email: planned.storedOrganizer, displayName: "" },
|
|
287
|
+
attendee: { email: planned.answeringAs, displayName: "" },
|
|
288
|
+
recurrenceId: scope === RecurrenceScope.This ? recurrenceId : "",
|
|
289
|
+
answer: update.answer,
|
|
290
|
+
},
|
|
291
|
+
],
|
|
292
|
+
};
|
|
293
|
+
};
|
|
294
|
+
/** The messages a delete owes, only on an event the reader organizes. */
|
|
295
|
+
export const planDeleteSends = (calendar, accounts, original, remaining, collectionTimezone) => {
|
|
296
|
+
const self = selfAddressSet(accounts.map((account) => account.email));
|
|
297
|
+
const storedOrganizer = organizerOf(calendar.master);
|
|
298
|
+
const guests = guestPeopleOf(calendar);
|
|
299
|
+
const organizer = self.has(storedOrganizer)
|
|
300
|
+
? accountByAddress(accounts, storedOrganizer)
|
|
301
|
+
: undefined;
|
|
302
|
+
if (!organizer || guests.length === 0)
|
|
303
|
+
return null;
|
|
304
|
+
const fanOut = planDeleteFanOut(remaining === null, guests.map((guest) => guest.email));
|
|
305
|
+
const source = remaining ?? original;
|
|
306
|
+
return {
|
|
307
|
+
from: organizer,
|
|
308
|
+
plans: fanOut.recipients.map((email) => {
|
|
309
|
+
const person = personOf(guests, email);
|
|
310
|
+
return {
|
|
311
|
+
method: fanOut.method,
|
|
312
|
+
calendarObjectId: original.calendarObjectId,
|
|
313
|
+
icalData: source.icalData,
|
|
314
|
+
collectionTimezone,
|
|
315
|
+
recipient: person,
|
|
316
|
+
attendee: person,
|
|
317
|
+
recurrenceId: "",
|
|
318
|
+
};
|
|
319
|
+
}),
|
|
320
|
+
};
|
|
321
|
+
};
|
package/dist/memory-store.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CalendarRepositories, CalendarWrite, ICalendarStore, ICalendarSuggestionRepository } from "@remit/data-ports";
|
|
1
|
+
import type { CalendarCommitReport, CalendarRepositories, CalendarWrite, ICalendarStore, ICalendarSuggestionRepository } from "@remit/data-ports";
|
|
2
2
|
import { NotFoundError } from "@remit/data-ports/errors";
|
|
3
3
|
export declare class MissingRow extends NotFoundError {
|
|
4
4
|
}
|
|
@@ -14,7 +14,7 @@ export declare class MemoryCalendarStore implements ICalendarStore {
|
|
|
14
14
|
urlSegment: string;
|
|
15
15
|
displayName: string;
|
|
16
16
|
color: "Cal1" | "Cal2" | "Cal3" | "Cal4" | "Cal5" | "Cal6";
|
|
17
|
-
componentSet: "VeventOnly";
|
|
17
|
+
componentSet: "VeventOnly" | "VtodoOnly";
|
|
18
18
|
source: "Default" | "UserCreated" | "MailDerived" | "Subscribed";
|
|
19
19
|
timezone: string;
|
|
20
20
|
syncSequence: number;
|
|
@@ -100,6 +100,6 @@ export declare class MemoryCalendarStore implements ICalendarStore {
|
|
|
100
100
|
private readonly feedTokenRepo;
|
|
101
101
|
get calendarSuggestion(): ICalendarSuggestionRepository;
|
|
102
102
|
get repos(): CalendarRepositories;
|
|
103
|
-
commit(writes: readonly CalendarWrite[]): Promise<
|
|
103
|
+
commit(writes: readonly CalendarWrite[]): Promise<CalendarCommitReport>;
|
|
104
104
|
private apply;
|
|
105
105
|
}
|
package/dist/memory-store.js
CHANGED
|
@@ -266,8 +266,14 @@ export class MemoryCalendarStore {
|
|
|
266
266
|
};
|
|
267
267
|
}
|
|
268
268
|
async commit(writes) {
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
const staleReprojections = [];
|
|
270
|
+
for (const write of writes) {
|
|
271
|
+
const applied = await this.apply(write);
|
|
272
|
+
if (!applied && write.kind === "ReprojectObject") {
|
|
273
|
+
staleReprojections.push(deriveCalendarObjectId(write.object.calendarId, write.object.resourceName));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return { staleReprojections };
|
|
271
277
|
}
|
|
272
278
|
async apply(write) {
|
|
273
279
|
switch (write.kind) {
|
|
@@ -279,32 +285,32 @@ export class MemoryCalendarStore {
|
|
|
279
285
|
syncSequence,
|
|
280
286
|
});
|
|
281
287
|
await this.eventIndexRepo.replaceForObject(calendarId, object.calendarObjectId, write.occurrences);
|
|
282
|
-
return;
|
|
288
|
+
return true;
|
|
283
289
|
}
|
|
284
290
|
case "ReprojectObject": {
|
|
285
291
|
const { calendarId, resourceName } = write.object;
|
|
286
292
|
const stored = await this.objectRepo.findByResourceName(calendarId, resourceName);
|
|
287
293
|
if (stored === null || stored.etag !== write.readEtag)
|
|
288
|
-
return;
|
|
294
|
+
return false;
|
|
289
295
|
await this.apply({
|
|
290
296
|
kind: "PutObject",
|
|
291
297
|
accountConfigId: write.accountConfigId,
|
|
292
298
|
object: write.object,
|
|
293
299
|
occurrences: write.occurrences,
|
|
294
300
|
});
|
|
295
|
-
return;
|
|
301
|
+
return true;
|
|
296
302
|
}
|
|
297
303
|
case "DeleteObject":
|
|
298
304
|
await this.eventIndexRepo.deleteForObject(write.calendarId, write.calendarObjectId);
|
|
299
305
|
await this.objectRepo.delete(write.calendarId, write.calendarObjectId);
|
|
300
306
|
await this.collectionRepo.bumpSyncSequence(write.accountConfigId, write.calendarId);
|
|
301
|
-
return;
|
|
307
|
+
return true;
|
|
302
308
|
case "SettleSuggestion":
|
|
303
309
|
await this.suggestionRepo.settle(write.accountConfigId, write.suggestionId, write.settle);
|
|
304
|
-
return;
|
|
310
|
+
return true;
|
|
305
311
|
case "UpdateCollection":
|
|
306
312
|
await this.collectionRepo.update(write.accountConfigId, write.calendarId, write.patch);
|
|
307
|
-
return;
|
|
313
|
+
return true;
|
|
308
314
|
case "DeleteCollection": {
|
|
309
315
|
for (const object of await this.objectRepo.listByCalendar(write.calendarId)) {
|
|
310
316
|
await this.eventIndexRepo.deleteForObject(write.calendarId, object.calendarObjectId);
|
|
@@ -312,7 +318,7 @@ export class MemoryCalendarStore {
|
|
|
312
318
|
}
|
|
313
319
|
await this.feedTokenRepo.delete(write.accountConfigId, write.calendarId);
|
|
314
320
|
await this.collectionRepo.delete(write.accountConfigId, write.calendarId);
|
|
315
|
-
return;
|
|
321
|
+
return true;
|
|
316
322
|
}
|
|
317
323
|
}
|
|
318
324
|
}
|