@remit/calendar-service 0.0.18 → 0.0.20
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/src/project.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import type { CalendarObjectItem } from "@remit/data-ports";
|
|
2
|
-
import { CalendarEventStatus, CalendarTransparency } from "@remit/domain-enums";
|
|
3
|
-
import ICAL from "ical.js";
|
|
4
|
-
import { type CalendarResult, calendarFailure } from "./errors.js";
|
|
5
|
-
import type { ParsedCalendar } from "./parse.js";
|
|
6
|
-
import { resolveTime, tzidOf } from "./time.js";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The columns `CalendarObject` carries beside its bytes. Every one of them is
|
|
10
|
-
* read out of the VEVENT here and nowhere else, so the row can only ever
|
|
11
|
-
* describe the `icalData` it was written with.
|
|
12
|
-
*/
|
|
13
|
-
export type CalendarObjectProjection = Pick<
|
|
14
|
-
CalendarObjectItem,
|
|
15
|
-
| "icalUid"
|
|
16
|
-
| "summary"
|
|
17
|
-
| "dtStart"
|
|
18
|
-
| "dtEnd"
|
|
19
|
-
| "allDay"
|
|
20
|
-
| "zoneCertainty"
|
|
21
|
-
| "status"
|
|
22
|
-
| "transparency"
|
|
23
|
-
| "hasRecurrence"
|
|
24
|
-
| "sequence"
|
|
25
|
-
>;
|
|
26
|
-
|
|
27
|
-
const STATUS_BY_ICAL: Record<string, CalendarObjectItem["status"]> = {
|
|
28
|
-
CONFIRMED: CalendarEventStatus.Confirmed,
|
|
29
|
-
TENTATIVE: CalendarEventStatus.Tentative,
|
|
30
|
-
CANCELLED: CalendarEventStatus.Cancelled,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const TRANSPARENCY_BY_ICAL: Record<string, CalendarObjectItem["transparency"]> =
|
|
34
|
-
{
|
|
35
|
-
OPAQUE: CalendarTransparency.Opaque,
|
|
36
|
-
TRANSPARENT: CalendarTransparency.Transparent,
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export const readString = (component: ICAL.Component, name: string): string => {
|
|
40
|
-
const value = component.getFirstPropertyValue(name);
|
|
41
|
-
return typeof value === "string" ? value : "";
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The fields that describe one VEVENT rather than the series it belongs to.
|
|
46
|
-
*
|
|
47
|
-
* Read per occurrence as well as per resource: an override VEVENT carries its
|
|
48
|
-
* own summary, status and transparency, and a range read that took them from
|
|
49
|
-
* the master would draw the old title over an edited instance and count a
|
|
50
|
-
* cancelled one as busy time.
|
|
51
|
-
*/
|
|
52
|
-
export type CalendarEventDisplay = Pick<
|
|
53
|
-
CalendarObjectItem,
|
|
54
|
-
"summary" | "status" | "transparency"
|
|
55
|
-
>;
|
|
56
|
-
|
|
57
|
-
export const projectEventDisplay = (
|
|
58
|
-
component: ICAL.Component,
|
|
59
|
-
): CalendarEventDisplay => ({
|
|
60
|
-
summary: readString(component, "summary"),
|
|
61
|
-
status:
|
|
62
|
-
STATUS_BY_ICAL[readString(component, "status").toUpperCase()] ??
|
|
63
|
-
CalendarEventStatus.Confirmed,
|
|
64
|
-
transparency:
|
|
65
|
-
TRANSPARENCY_BY_ICAL[readString(component, "transp").toUpperCase()] ??
|
|
66
|
-
CalendarTransparency.Opaque,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Whether the resource expands to more than its master instance — an RRULE, an
|
|
71
|
-
* RDATE, or an override VEVENT. The overrides count: a client that edits one
|
|
72
|
-
* instance of a series and then deletes the rule leaves a resource with nothing
|
|
73
|
-
* but a master and its exceptions, and every one of those is still an
|
|
74
|
-
* occurrence somebody has to see.
|
|
75
|
-
*/
|
|
76
|
-
export const hasRecurrence = (calendar: ParsedCalendar): boolean =>
|
|
77
|
-
calendar.master.hasProperty("rrule") ||
|
|
78
|
-
calendar.master.hasProperty("rdate") ||
|
|
79
|
-
calendar.overrides.length > 0;
|
|
80
|
-
|
|
81
|
-
export const projectCalendar = (
|
|
82
|
-
calendar: ParsedCalendar,
|
|
83
|
-
collectionTimezone: string,
|
|
84
|
-
): CalendarResult<CalendarObjectProjection> => {
|
|
85
|
-
const event = new ICAL.Event(calendar.master);
|
|
86
|
-
const start = resolveTime(
|
|
87
|
-
event.startDate,
|
|
88
|
-
tzidOf(calendar.master.getFirstProperty("dtstart")),
|
|
89
|
-
collectionTimezone,
|
|
90
|
-
);
|
|
91
|
-
const end = resolveTime(
|
|
92
|
-
event.endDate,
|
|
93
|
-
tzidOf(
|
|
94
|
-
calendar.master.getFirstProperty("dtend") ??
|
|
95
|
-
calendar.master.getFirstProperty("dtstart"),
|
|
96
|
-
),
|
|
97
|
-
collectionTimezone,
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
if (end.instantMs < start.instantMs) {
|
|
101
|
-
return calendarFailure(
|
|
102
|
-
"BackwardsEnd",
|
|
103
|
-
`the event ends at ${end.isoUtc}, before it starts at ${start.isoUtc}`,
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const sequence = calendar.master.getFirstPropertyValue("sequence");
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
ok: true,
|
|
111
|
-
value: {
|
|
112
|
-
icalUid: calendar.uid,
|
|
113
|
-
dtStart: start.isoOffset,
|
|
114
|
-
dtEnd: end.isoOffset,
|
|
115
|
-
allDay: start.isDate,
|
|
116
|
-
zoneCertainty: start.certainty,
|
|
117
|
-
...projectEventDisplay(calendar.master),
|
|
118
|
-
hasRecurrence: hasRecurrence(calendar),
|
|
119
|
-
sequence: typeof sequence === "number" ? sequence : 0,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
};
|
package/src/put.test.ts
DELETED
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { describe, it } from "node:test";
|
|
3
|
-
import { computeEtag } from "./etag.js";
|
|
4
|
-
import { asLf, singleEvent } from "./fixtures.js";
|
|
5
|
-
import { MemoryCalendarStore, MissingRow } from "./memory-store.js";
|
|
6
|
-
import { parseCalendar, serializeCalendar } from "./parse.js";
|
|
7
|
-
import {
|
|
8
|
-
DEFAULT_CALENDAR_URL_SEGMENT,
|
|
9
|
-
deleteCalendarObject,
|
|
10
|
-
provisionDefaultCalendar,
|
|
11
|
-
putCalendarObject,
|
|
12
|
-
} from "./put.js";
|
|
13
|
-
|
|
14
|
-
const ACCOUNT_CONFIG_ID = "account-config-1";
|
|
15
|
-
|
|
16
|
-
const provisioned = async (): Promise<{
|
|
17
|
-
store: MemoryCalendarStore;
|
|
18
|
-
calendarId: string;
|
|
19
|
-
}> => {
|
|
20
|
-
const store = new MemoryCalendarStore();
|
|
21
|
-
const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
|
|
22
|
-
return { store, calendarId: collection.calendarId };
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const RESOURCE = singleEvent(
|
|
26
|
-
"DTSTART:20260826T090000Z",
|
|
27
|
-
"DTEND:20260826T100000Z",
|
|
28
|
-
"SUMMARY:Quarterly review",
|
|
29
|
-
);
|
|
30
|
-
|
|
31
|
-
describe("provisionDefaultCalendar", () => {
|
|
32
|
-
it("gives an account config a default collection at the default segment", async () => {
|
|
33
|
-
const store = new MemoryCalendarStore();
|
|
34
|
-
|
|
35
|
-
const collection = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
|
|
36
|
-
|
|
37
|
-
assert.equal(collection.urlSegment, DEFAULT_CALENDAR_URL_SEGMENT);
|
|
38
|
-
assert.equal(collection.source, "Default");
|
|
39
|
-
assert.equal(collection.displayName, "Calendar");
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it("returns the same collection on a second first use", async () => {
|
|
43
|
-
const store = new MemoryCalendarStore();
|
|
44
|
-
|
|
45
|
-
const first = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
|
|
46
|
-
const second = await provisionDefaultCalendar(store, ACCOUNT_CONFIG_ID);
|
|
47
|
-
|
|
48
|
-
assert.equal(second.calendarId, first.calendarId);
|
|
49
|
-
assert.equal(store.collections.size, 1);
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe("putCalendarObject", () => {
|
|
54
|
-
it("stores the bytes it was given, untouched", async () => {
|
|
55
|
-
const { store, calendarId } = await provisioned();
|
|
56
|
-
|
|
57
|
-
const result = await putCalendarObject(store, {
|
|
58
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
59
|
-
calendarId,
|
|
60
|
-
resourceName: "review.ics",
|
|
61
|
-
icalData: RESOURCE,
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
assert.ok(result.ok);
|
|
65
|
-
assert.equal(result.value.icalData, RESOURCE);
|
|
66
|
-
assert.equal(result.value.summary, "Quarterly review");
|
|
67
|
-
assert.equal(result.value.icalUid, "fixture@example.com");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("stores the input bytes rather than a reserialization of them", async () => {
|
|
71
|
-
// The etag is a digest of what the writer sent. ical.js refolds and
|
|
72
|
-
// reorders on serialize, so a write path that stored its own
|
|
73
|
-
// reserialization would hand back a tag for bytes nobody wrote, and every
|
|
74
|
-
// client's cached copy would miss on the next read.
|
|
75
|
-
const { store, calendarId } = await provisioned();
|
|
76
|
-
const source = singleEvent(
|
|
77
|
-
"DTSTART:20260826T090000Z",
|
|
78
|
-
"DTEND:20260826T100000Z",
|
|
79
|
-
"SUMMARY:A summary long enough that RFC 5545 line folding rewrites it when ical.js serializes the event again",
|
|
80
|
-
"X-MICROSOFT-CDO-BUSYSTATUS:BUSY",
|
|
81
|
-
);
|
|
82
|
-
const parsed = await parseCalendar(source);
|
|
83
|
-
assert.ok(parsed.ok);
|
|
84
|
-
const reserialized = serializeCalendar(parsed.value.component);
|
|
85
|
-
assert.notEqual(
|
|
86
|
-
reserialized,
|
|
87
|
-
source,
|
|
88
|
-
"the fixture survives reserialization unchanged, so it proves nothing",
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const result = await putCalendarObject(store, {
|
|
92
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
93
|
-
calendarId,
|
|
94
|
-
resourceName: "folded.ics",
|
|
95
|
-
icalData: source,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
assert.ok(result.ok);
|
|
99
|
-
assert.equal(result.value.icalData, source);
|
|
100
|
-
assert.equal(result.value.etag, computeEtag(source));
|
|
101
|
-
assert.notEqual(result.value.etag, computeEtag(reserialized));
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("stores an LF-only resource as it arrived, without normalizing it", async () => {
|
|
105
|
-
// The store keeps what the writer sent. Rewriting line endings on the way
|
|
106
|
-
// in would give the resource an etag for bytes nobody wrote, and the
|
|
107
|
-
// writer's own If-Match would miss on its very next request.
|
|
108
|
-
const { store, calendarId } = await provisioned();
|
|
109
|
-
const source = asLf(RESOURCE);
|
|
110
|
-
|
|
111
|
-
const result = await putCalendarObject(store, {
|
|
112
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
113
|
-
calendarId,
|
|
114
|
-
resourceName: "lf.ics",
|
|
115
|
-
icalData: source,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
assert.ok(result.ok);
|
|
119
|
-
assert.equal(result.value.icalData, source);
|
|
120
|
-
assert.ok(!result.value.icalData.includes("\r"));
|
|
121
|
-
assert.equal(result.value.etag, computeEtag(source));
|
|
122
|
-
assert.equal(result.value.summary, "Quarterly review");
|
|
123
|
-
assert.equal(result.value.dtStart, "2026-08-26T09:00:00+00:00");
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("writes the occurrence rows the resource expands to", async () => {
|
|
127
|
-
const { store, calendarId } = await provisioned();
|
|
128
|
-
|
|
129
|
-
const result = await putCalendarObject(store, {
|
|
130
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
131
|
-
calendarId,
|
|
132
|
-
resourceName: "series.ics",
|
|
133
|
-
icalData: singleEvent(
|
|
134
|
-
"DTSTART:20260826T090000Z",
|
|
135
|
-
"DTEND:20260826T100000Z",
|
|
136
|
-
"RRULE:FREQ=WEEKLY;COUNT=3",
|
|
137
|
-
),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
assert.ok(result.ok);
|
|
141
|
-
assert.equal(
|
|
142
|
-
store.occurrences.get(result.value.calendarObjectId)?.length,
|
|
143
|
-
3,
|
|
144
|
-
);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("replaces the occurrences of a resource it rewrites", async () => {
|
|
148
|
-
const { store, calendarId } = await provisioned();
|
|
149
|
-
const input = {
|
|
150
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
151
|
-
calendarId,
|
|
152
|
-
resourceName: "series.ics",
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
await putCalendarObject(store, {
|
|
156
|
-
...input,
|
|
157
|
-
icalData: singleEvent(
|
|
158
|
-
"DTSTART:20260826T090000Z",
|
|
159
|
-
"DTEND:20260826T100000Z",
|
|
160
|
-
"RRULE:FREQ=WEEKLY;COUNT=5",
|
|
161
|
-
),
|
|
162
|
-
});
|
|
163
|
-
const result = await putCalendarObject(store, {
|
|
164
|
-
...input,
|
|
165
|
-
icalData: singleEvent(
|
|
166
|
-
"DTSTART:20260826T090000Z",
|
|
167
|
-
"DTEND:20260826T100000Z",
|
|
168
|
-
"RRULE:FREQ=WEEKLY;COUNT=2",
|
|
169
|
-
),
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
assert.ok(result.ok);
|
|
173
|
-
assert.equal(store.objects.size, 1);
|
|
174
|
-
assert.equal(
|
|
175
|
-
store.occurrences.get(result.value.calendarObjectId)?.length,
|
|
176
|
-
2,
|
|
177
|
-
);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("stamps the collection's new sequence on the resource it wrote", async () => {
|
|
181
|
-
const { store, calendarId } = await provisioned();
|
|
182
|
-
|
|
183
|
-
const first = await putCalendarObject(store, {
|
|
184
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
185
|
-
calendarId,
|
|
186
|
-
resourceName: "one.ics",
|
|
187
|
-
icalData: RESOURCE,
|
|
188
|
-
});
|
|
189
|
-
const second = await putCalendarObject(store, {
|
|
190
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
191
|
-
calendarId,
|
|
192
|
-
resourceName: "two.ics",
|
|
193
|
-
icalData: singleEvent(
|
|
194
|
-
"DTSTART:20260827T090000Z",
|
|
195
|
-
"DTEND:20260827T100000Z",
|
|
196
|
-
),
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
assert.ok(first.ok);
|
|
200
|
-
assert.ok(second.ok);
|
|
201
|
-
assert.equal(first.value.syncSequence, 1);
|
|
202
|
-
assert.equal(second.value.syncSequence, 2);
|
|
203
|
-
assert.equal(store.collections.get(calendarId)?.syncSequence, 2);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it("computes the etag over the stored bytes", async () => {
|
|
207
|
-
const { store, calendarId } = await provisioned();
|
|
208
|
-
|
|
209
|
-
const result = await putCalendarObject(store, {
|
|
210
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
211
|
-
calendarId,
|
|
212
|
-
resourceName: "review.ics",
|
|
213
|
-
icalData: RESOURCE,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
assert.ok(result.ok);
|
|
217
|
-
assert.match(result.value.etag, /^[0-9a-f]{64}$/);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it("refuses a resource the collection does not store, and writes nothing", async () => {
|
|
221
|
-
const { store, calendarId } = await provisioned();
|
|
222
|
-
|
|
223
|
-
const result = await putCalendarObject(store, {
|
|
224
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
225
|
-
calendarId,
|
|
226
|
-
resourceName: "todo.ics",
|
|
227
|
-
icalData: [
|
|
228
|
-
"BEGIN:VCALENDAR",
|
|
229
|
-
"VERSION:2.0",
|
|
230
|
-
"BEGIN:VTODO",
|
|
231
|
-
"UID:todo@example.com",
|
|
232
|
-
"END:VTODO",
|
|
233
|
-
"END:VCALENDAR",
|
|
234
|
-
"",
|
|
235
|
-
].join("\r\n"),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
assert.ok(!result.ok);
|
|
239
|
-
assert.equal(result.error.code, "UnsupportedComponent");
|
|
240
|
-
assert.equal(store.objects.size, 0);
|
|
241
|
-
assert.equal(store.collections.get(calendarId)?.syncSequence, 0);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it("refuses an event that ends before it starts", async () => {
|
|
245
|
-
const { store, calendarId } = await provisioned();
|
|
246
|
-
|
|
247
|
-
const result = await putCalendarObject(store, {
|
|
248
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
249
|
-
calendarId,
|
|
250
|
-
resourceName: "backwards.ics",
|
|
251
|
-
icalData: singleEvent(
|
|
252
|
-
"DTSTART:20260826T100000Z",
|
|
253
|
-
"DTEND:20260826T090000Z",
|
|
254
|
-
),
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
assert.ok(!result.ok);
|
|
258
|
-
assert.equal(result.error.code, "BackwardsEnd");
|
|
259
|
-
assert.equal(store.objects.size, 0);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it("fails loudly when the collection does not exist", async () => {
|
|
263
|
-
const store = new MemoryCalendarStore();
|
|
264
|
-
|
|
265
|
-
await assert.rejects(
|
|
266
|
-
putCalendarObject(store, {
|
|
267
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
268
|
-
calendarId: "no-such-calendar",
|
|
269
|
-
resourceName: "review.ics",
|
|
270
|
-
icalData: RESOURCE,
|
|
271
|
-
}),
|
|
272
|
-
MissingRow,
|
|
273
|
-
);
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("projects the event in the collection's timezone", async () => {
|
|
277
|
-
const store = new MemoryCalendarStore();
|
|
278
|
-
const collection = await store.repos.calendarCollection.create({
|
|
279
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
280
|
-
urlSegment: "berlin",
|
|
281
|
-
displayName: "Berlin",
|
|
282
|
-
timezone: "Europe/Berlin",
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
const result = await putCalendarObject(store, {
|
|
286
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
287
|
-
calendarId: collection.calendarId,
|
|
288
|
-
resourceName: "floating.ics",
|
|
289
|
-
icalData: singleEvent("DTSTART:20260826T090000", "DTEND:20260826T100000"),
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
assert.ok(result.ok);
|
|
293
|
-
assert.equal(result.value.dtStart, "2026-08-26T09:00:00+02:00");
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
describe("deleteCalendarObject", () => {
|
|
298
|
-
it("takes the occurrences with the resource and bumps the collection", async () => {
|
|
299
|
-
const { store, calendarId } = await provisioned();
|
|
300
|
-
const written = await putCalendarObject(store, {
|
|
301
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
302
|
-
calendarId,
|
|
303
|
-
resourceName: "review.ics",
|
|
304
|
-
icalData: RESOURCE,
|
|
305
|
-
});
|
|
306
|
-
assert.ok(written.ok);
|
|
307
|
-
|
|
308
|
-
await deleteCalendarObject(store, {
|
|
309
|
-
accountConfigId: ACCOUNT_CONFIG_ID,
|
|
310
|
-
calendarId,
|
|
311
|
-
calendarObjectId: written.value.calendarObjectId,
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
assert.equal(store.objects.size, 0);
|
|
315
|
-
assert.equal(store.occurrences.size, 0);
|
|
316
|
-
assert.equal(store.collections.get(calendarId)?.syncSequence, 2);
|
|
317
|
-
});
|
|
318
|
-
});
|
package/src/put.ts
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CalendarCollectionItem,
|
|
3
|
-
CalendarObjectItem,
|
|
4
|
-
DeleteCalendarObjectWrite,
|
|
5
|
-
ICalendarStore,
|
|
6
|
-
PutCalendarObjectWrite,
|
|
7
|
-
} from "@remit/data-ports";
|
|
8
|
-
import { deriveCalendarObjectId } from "@remit/data-ports/id";
|
|
9
|
-
import { CalendarSource } from "@remit/domain-enums";
|
|
10
|
-
import type { CalendarResult } from "./errors.js";
|
|
11
|
-
import { computeEtag } from "./etag.js";
|
|
12
|
-
import { expandCalendar } from "./expand.js";
|
|
13
|
-
import { parseCalendar } from "./parse.js";
|
|
14
|
-
import { projectCalendar } from "./project.js";
|
|
15
|
-
|
|
16
|
-
/** URL segment of the collection every account config is provisioned with. */
|
|
17
|
-
export const DEFAULT_CALENDAR_URL_SEGMENT = "default";
|
|
18
|
-
|
|
19
|
-
export interface PutCalendarObjectInput {
|
|
20
|
-
accountConfigId: string;
|
|
21
|
-
calendarId: string;
|
|
22
|
-
/** Last path segment of the resource's URL, e.g. `"a1b2c3.ics"`. */
|
|
23
|
-
resourceName: string;
|
|
24
|
-
/** The VCALENDAR text as it arrived, stored byte-for-byte. */
|
|
25
|
-
icalData: string;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Turns one calendar resource into the write that stores it, read in the
|
|
30
|
-
* collection's timezone. Every caller that stores an event goes through here —
|
|
31
|
-
* a REST handler, a DAV PUT, an accepted suggestion, a feed refresh — because
|
|
32
|
-
* the object row, its occurrence rows and the collection's sequence bump are
|
|
33
|
-
* one fact, and a caller that writes any of them alone leaves the store
|
|
34
|
-
* describing a calendar that does not exist.
|
|
35
|
-
*
|
|
36
|
-
* The bytes are stored exactly as given: parsing is for validating and
|
|
37
|
-
* projecting them, never for rewriting them. Refusing the resource is a
|
|
38
|
-
* returned value — the input is client-supplied, so malformed iCalendar is an
|
|
39
|
-
* outcome the caller renders, not a fault.
|
|
40
|
-
*/
|
|
41
|
-
export const prepareCalendarObject = async (
|
|
42
|
-
collection: Pick<CalendarCollectionItem, "timezone">,
|
|
43
|
-
input: PutCalendarObjectInput,
|
|
44
|
-
): Promise<CalendarResult<PutCalendarObjectWrite>> => {
|
|
45
|
-
const parsed = await parseCalendar(input.icalData);
|
|
46
|
-
if (!parsed.ok) return parsed;
|
|
47
|
-
|
|
48
|
-
const projection = projectCalendar(parsed.value, collection.timezone);
|
|
49
|
-
if (!projection.ok) return projection;
|
|
50
|
-
|
|
51
|
-
const expansion = expandCalendar(parsed.value, collection.timezone);
|
|
52
|
-
return {
|
|
53
|
-
ok: true,
|
|
54
|
-
value: {
|
|
55
|
-
kind: "PutObject",
|
|
56
|
-
accountConfigId: input.accountConfigId,
|
|
57
|
-
object: {
|
|
58
|
-
...projection.value,
|
|
59
|
-
calendarId: input.calendarId,
|
|
60
|
-
resourceName: input.resourceName,
|
|
61
|
-
icalData: input.icalData,
|
|
62
|
-
etag: computeEtag(input.icalData),
|
|
63
|
-
expandedThrough: expansion.expandedThrough,
|
|
64
|
-
},
|
|
65
|
-
occurrences: expansion.occurrences,
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/** The resource a committed put wrote, under the id its name derives to. */
|
|
71
|
-
export const readWrittenObject = (
|
|
72
|
-
store: ICalendarStore,
|
|
73
|
-
write: PutCalendarObjectWrite,
|
|
74
|
-
): Promise<CalendarObjectItem> =>
|
|
75
|
-
store.repos.calendarObject.get(
|
|
76
|
-
write.object.calendarId,
|
|
77
|
-
deriveCalendarObjectId(write.object.calendarId, write.object.resourceName),
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
/** Writes one calendar resource into its collection. */
|
|
81
|
-
export const putCalendarObject = async (
|
|
82
|
-
store: ICalendarStore,
|
|
83
|
-
input: PutCalendarObjectInput,
|
|
84
|
-
): Promise<CalendarResult<CalendarObjectItem>> => {
|
|
85
|
-
const collection = await store.repos.calendarCollection.get(
|
|
86
|
-
input.accountConfigId,
|
|
87
|
-
input.calendarId,
|
|
88
|
-
);
|
|
89
|
-
const write = await prepareCalendarObject(collection, input);
|
|
90
|
-
if (!write.ok) return write;
|
|
91
|
-
await store.commit([write.value]);
|
|
92
|
-
return { ok: true, value: await readWrittenObject(store, write.value) };
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Removes a resource and the occurrences it produced — an object dropped
|
|
97
|
-
* without its occurrence rows leaves a calendar showing events no resource
|
|
98
|
-
* backs, which nothing later can attribute or clean up.
|
|
99
|
-
*/
|
|
100
|
-
export const deleteCalendarObjectWrite = (input: {
|
|
101
|
-
accountConfigId: string;
|
|
102
|
-
calendarId: string;
|
|
103
|
-
calendarObjectId: string;
|
|
104
|
-
}): DeleteCalendarObjectWrite => ({ kind: "DeleteObject", ...input });
|
|
105
|
-
|
|
106
|
-
export const deleteCalendarObject = async (
|
|
107
|
-
store: ICalendarStore,
|
|
108
|
-
input: {
|
|
109
|
-
accountConfigId: string;
|
|
110
|
-
calendarId: string;
|
|
111
|
-
calendarObjectId: string;
|
|
112
|
-
},
|
|
113
|
-
): Promise<void> => {
|
|
114
|
-
await store.commit([deleteCalendarObjectWrite(input)]);
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* The collection an account config stores events in until someone makes
|
|
119
|
-
* another. Safe to call on every request that needs a calendar: `calendarId` is
|
|
120
|
-
* derived from the account config and the URL segment, so a second call returns
|
|
121
|
-
* the collection the first one made rather than a rival copy of it.
|
|
122
|
-
*/
|
|
123
|
-
export const provisionDefaultCalendar = (
|
|
124
|
-
store: ICalendarStore,
|
|
125
|
-
accountConfigId: string,
|
|
126
|
-
displayName = "Calendar",
|
|
127
|
-
): Promise<CalendarCollectionItem> =>
|
|
128
|
-
store.repos.calendarCollection.create({
|
|
129
|
-
accountConfigId,
|
|
130
|
-
urlSegment: DEFAULT_CALENDAR_URL_SEGMENT,
|
|
131
|
-
displayName,
|
|
132
|
-
source: CalendarSource.Default,
|
|
133
|
-
});
|