@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.
- package/dist/accept.d.ts +71 -0
- package/dist/accept.js +159 -0
- package/dist/build.d.ts +79 -0
- package/dist/build.js +196 -0
- package/dist/correct.d.ts +4 -0
- package/dist/correct.js +22 -0
- package/dist/errors.d.ts +56 -0
- package/dist/errors.js +1 -0
- package/dist/etag.d.ts +9 -0
- package/{src/etag.ts → dist/etag.js} +1 -3
- package/dist/expand.d.ts +73 -0
- package/dist/expand.js +188 -0
- package/dist/feed.d.ts +81 -0
- package/dist/feed.js +157 -0
- package/dist/fixtures.d.ts +19 -0
- package/dist/fixtures.js +37 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +15 -0
- package/dist/memory-store.d.ts +105 -0
- package/dist/memory-store.js +319 -0
- package/dist/parse.d.ts +39 -0
- package/dist/parse.js +87 -0
- package/dist/project.d.ts +30 -0
- package/dist/project.js +57 -0
- package/dist/put.d.ts +52 -0
- package/dist/put.js +78 -0
- package/dist/reopen.d.ts +14 -0
- package/dist/reopen.js +92 -0
- package/dist/scope.d.ts +70 -0
- package/dist/scope.js +687 -0
- package/dist/subscribe.d.ts +96 -0
- package/dist/subscribe.js +298 -0
- package/dist/suggest.d.ts +74 -0
- package/dist/suggest.js +138 -0
- package/dist/time.d.ts +76 -0
- package/dist/time.js +164 -0
- package/dist/window.d.ts +85 -0
- package/dist/window.js +161 -0
- package/package.json +40 -35
- package/src/accept.test.ts +0 -403
- package/src/accept.ts +0 -250
- package/src/build.test.ts +0 -113
- package/src/build.ts +0 -311
- package/src/correct.test.ts +0 -173
- package/src/correct.ts +0 -40
- package/src/errors.ts +0 -62
- package/src/etag.test.ts +0 -54
- package/src/expand.test.ts +0 -361
- package/src/expand.ts +0 -303
- package/src/feed.test.ts +0 -334
- package/src/feed.ts +0 -203
- package/src/feeds/google-secret-address.ics +0 -99
- package/src/fixtures.ts +0 -52
- package/src/index.ts +0 -128
- package/src/memory-store.ts +0 -485
- package/src/parse.test.ts +0 -264
- package/src/parse.ts +0 -138
- package/src/project.test.ts +0 -247
- package/src/project.ts +0 -122
- package/src/put.test.ts +0 -318
- package/src/put.ts +0 -133
- package/src/reopen.test.ts +0 -529
- package/src/reopen.ts +0 -199
- package/src/scope.test.ts +0 -1421
- package/src/scope.ts +0 -1003
- package/src/subscribe.test.ts +0 -582
- package/src/subscribe.ts +0 -433
- package/src/suggest.test.ts +0 -376
- package/src/suggest.ts +0 -244
- package/src/time.ts +0 -260
- package/src/window.test.ts +0 -648
- package/src/window.ts +0 -281
- 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
|
+
});
|
package/dist/reopen.d.ts
ADDED
|
@@ -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
|
+
};
|
package/dist/scope.d.ts
ADDED
|
@@ -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 {};
|