@remit/backend 0.0.142 → 0.0.144
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/dev-server/content-handler.d.ts +38 -0
- package/dist/dev-server/content-handler.js +46 -0
- package/dist/src/api.d.ts +7 -0
- package/dist/src/api.js +144 -0
- package/dist/src/auth.d.ts +30 -0
- package/dist/src/auth.js +73 -0
- package/dist/src/config/msoauth.d.ts +30 -0
- package/dist/src/config/msoauth.js +30 -0
- package/{src/data-backend.ts → dist/src/data-backend.d.ts} +2 -4
- package/dist/src/data-backend.js +12 -0
- package/{src/derive/autoMoved.ts → dist/src/derive/autoMoved.d.ts} +1 -29
- package/dist/src/derive/autoMoved.js +49 -0
- package/dist/src/derive/contentSignature.d.ts +41 -0
- package/{src/derive/contentSignature.ts → dist/src/derive/contentSignature.js} +27 -72
- package/dist/src/derive/contentUrl.d.ts +46 -0
- package/dist/src/derive/contentUrl.js +46 -0
- package/{src/derive/deriveMuted.ts → dist/src/derive/deriveMuted.d.ts} +1 -3
- package/dist/src/derive/deriveMuted.js +7 -0
- package/dist/src/derive/enrichThreadRows.d.ts +118 -0
- package/dist/src/derive/enrichThreadRows.js +151 -0
- package/dist/src/derive/filterThreadCriteria.d.ts +23 -0
- package/dist/src/derive/filterThreadCriteria.js +24 -0
- package/{src/derive/pendingMoveCounts.ts → dist/src/derive/pendingMoveCounts.d.ts} +5 -53
- package/dist/src/derive/pendingMoveCounts.js +53 -0
- package/{src/derive/senderTrust.ts → dist/src/derive/senderTrust.d.ts} +1 -9
- package/dist/src/derive/senderTrust.js +19 -0
- package/dist/src/error.d.ts +21 -0
- package/dist/src/error.js +85 -0
- package/dist/src/handlers/account-guards.d.ts +36 -0
- package/dist/src/handlers/account-guards.js +107 -0
- package/dist/src/handlers/account-oauth.d.ts +14 -0
- package/dist/src/handlers/account-oauth.js +387 -0
- package/dist/src/handlers/account-overrides.d.ts +53 -0
- package/dist/src/handlers/account-overrides.js +233 -0
- package/dist/src/handlers/account-signature.d.ts +28 -0
- package/dist/src/handlers/account-signature.js +85 -0
- package/dist/src/handlers/account.d.ts +6 -0
- package/dist/src/handlers/account.js +350 -0
- package/dist/src/handlers/address.d.ts +35 -0
- package/dist/src/handlers/address.js +141 -0
- package/dist/src/handlers/calendar-event.d.ts +70 -0
- package/dist/src/handlers/calendar-event.js +425 -0
- package/dist/src/handlers/calendar-feed.d.ts +47 -0
- package/dist/src/handlers/calendar-feed.js +192 -0
- package/dist/src/handlers/calendar-suggestion.d.ts +47 -0
- package/dist/src/handlers/calendar-suggestion.js +212 -0
- package/dist/src/handlers/calendar.d.ts +116 -0
- package/dist/src/handlers/calendar.js +330 -0
- package/dist/src/handlers/config.d.ts +43 -0
- package/dist/src/handlers/config.js +176 -0
- package/dist/src/handlers/ensure-account-config.d.ts +9 -0
- package/dist/src/handlers/ensure-account-config.js +27 -0
- package/dist/src/handlers/filter.d.ts +3 -0
- package/dist/src/handlers/filter.js +103 -0
- package/dist/src/handlers/folder-role-appointments.d.ts +68 -0
- package/dist/src/handlers/folder-role-appointments.js +168 -0
- package/dist/src/handlers/folder-role-labels.d.ts +26 -0
- package/dist/src/handlers/folder-role-labels.js +40 -0
- package/dist/src/handlers/folder-role.d.ts +9 -0
- package/dist/src/handlers/folder-role.js +40 -0
- package/dist/src/handlers/index.d.ts +2 -0
- package/dist/src/handlers/index.js +60 -0
- package/dist/src/handlers/label.d.ts +43 -0
- package/dist/src/handlers/label.js +114 -0
- package/dist/src/handlers/mailbox-bindings.d.ts +41 -0
- package/dist/src/handlers/mailbox-bindings.js +61 -0
- package/dist/src/handlers/mailbox.d.ts +23 -0
- package/dist/src/handlers/mailbox.js +219 -0
- package/dist/src/handlers/me.d.ts +2 -0
- package/dist/src/handlers/me.js +138 -0
- package/dist/src/handlers/message.d.ts +84 -0
- package/dist/src/handlers/message.js +607 -0
- package/dist/src/handlers/organize.d.ts +10 -0
- package/dist/src/handlers/organize.js +112 -0
- package/dist/src/handlers/outbox-attachment.d.ts +23 -0
- package/dist/src/handlers/outbox-attachment.js +81 -0
- package/dist/src/handlers/outbox.d.ts +4 -0
- package/dist/src/handlers/outbox.js +144 -0
- package/dist/src/handlers/sync-progress.d.ts +46 -0
- package/dist/src/handlers/sync-progress.js +63 -0
- package/dist/src/handlers/sync.d.ts +28 -0
- package/dist/src/handlers/sync.js +82 -0
- package/dist/src/handlers/system-update.d.ts +2 -0
- package/dist/src/handlers/system-update.js +172 -0
- package/dist/src/handlers/thread.d.ts +148 -0
- package/dist/src/handlers/thread.js +233 -0
- package/dist/src/handlers/unified-threads.d.ts +247 -0
- package/dist/src/handlers/unified-threads.js +409 -0
- package/{src/handlers/vip-suggestions.ts → dist/src/handlers/vip-suggestions.d.ts} +1 -13
- package/dist/src/handlers/vip-suggestions.js +16 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +37 -0
- package/{src/json.ts → dist/src/json.d.ts} +1 -4
- package/dist/src/json.js +10 -0
- package/dist/src/jwt-auth.d.ts +18 -0
- package/dist/src/jwt-auth.js +97 -0
- package/dist/src/request-context.d.ts +9 -0
- package/dist/src/request-context.js +22 -0
- package/dist/src/request.d.ts +3 -0
- package/dist/src/request.js +7 -0
- package/dist/src/response.d.ts +27 -0
- package/dist/src/response.js +87 -0
- package/dist/src/service/account-deletion.d.ts +22 -0
- package/dist/src/service/account-deletion.js +39 -0
- package/dist/src/service/compose-sqlite.d.ts +2 -0
- package/dist/src/service/compose-sqlite.js +11 -0
- package/dist/src/service/create-remit-client.d.ts +95 -0
- package/dist/src/service/create-remit-client.js +207 -0
- package/dist/src/service/data-client.d.ts +12 -0
- package/dist/src/service/data-client.js +44 -0
- package/dist/src/service/export-identity.d.ts +3 -0
- package/dist/src/service/export-identity.js +35 -0
- package/dist/src/service/fire-and-forget.d.ts +54 -0
- package/dist/src/service/fire-and-forget.js +38 -0
- package/dist/src/service/organize.d.ts +184 -0
- package/dist/src/service/organize.js +384 -0
- package/dist/src/service/refusal.d.ts +4 -0
- package/dist/src/service/refusal.js +20 -0
- package/dist/src/service/sender-category.d.ts +51 -0
- package/{src/service/sender-category.ts → dist/src/service/sender-category.js} +40 -81
- package/dist/src/service/sqs.d.ts +1 -0
- package/{src/service/sqs.ts → dist/src/service/sqs.js} +0 -1
- package/dist/src/service/trigger-sync.d.ts +51 -0
- package/dist/src/service/trigger-sync.js +39 -0
- package/dist/src/types.d.ts +49 -0
- package/dist/src/types.js +1 -0
- package/package.json +113 -98
- package/.env.test +0 -4
- package/dev-server/content-auth.test.ts +0 -113
- package/dev-server/content-auth.ts +0 -54
- package/dev-server/content-handler.test.ts +0 -105
- package/dev-server/content-handler.ts +0 -79
- package/dev-server/content-path.test.ts +0 -37
- package/dev-server/content-path.ts +0 -27
- package/dev-server/cors.test.ts +0 -48
- package/dev-server/cors.ts +0 -25
- package/dev-server/keep-alive.test.ts +0 -24
- package/dev-server/lambda-helpers.ts +0 -69
- package/dev-server/log-output.test.ts +0 -173
- package/dev-server/oauth-callback-route.test.ts +0 -66
- package/dev-server/relational-health.test.ts +0 -83
- package/dev-server/relational-health.ts +0 -39
- package/dev-server/server.ts +0 -384
- package/dev-server/sync-age.test.ts +0 -91
- package/dev-server/sync-age.ts +0 -55
- package/dev-server/upload-handler.ts +0 -132
- package/dev-server/upload-route.test.ts +0 -196
- package/scripts/backfill-classification.ts +0 -91
- package/scripts/backfill-list-id.ts +0 -83
- package/scripts/config-save.ts +0 -128
- package/src/auth.test.ts +0 -12
- package/src/auth.ts +0 -92
- package/src/config/msoauth.test.ts +0 -48
- package/src/config/msoauth.ts +0 -48
- package/src/data-backend.test.ts +0 -23
- package/src/derive/autoMoved.test.ts +0 -104
- package/src/derive/contentSignature.test.ts +0 -161
- package/src/derive/contentUrl.test.ts +0 -172
- package/src/derive/contentUrl.ts +0 -86
- package/src/derive/deriveMuted.test.ts +0 -34
- package/src/derive/enrichThreadRows.test.ts +0 -360
- package/src/derive/enrichThreadRows.ts +0 -234
- package/src/derive/filterThreadCriteria.test.ts +0 -126
- package/src/derive/filterThreadCriteria.ts +0 -51
- package/src/derive/senderTrust.test.ts +0 -67
- package/src/error.test.ts +0 -201
- package/src/error.ts +0 -123
- package/src/handlers/account-guards.test.ts +0 -67
- package/src/handlers/account-guards.ts +0 -169
- package/src/handlers/account-oauth-callback.test.ts +0 -155
- package/src/handlers/account-oauth-consent.test.ts +0 -336
- package/src/handlers/account-oauth-start.test.ts +0 -191
- package/src/handlers/account-oauth.test.ts +0 -439
- package/src/handlers/account-oauth.ts +0 -635
- package/src/handlers/account-overrides.test.ts +0 -88
- package/src/handlers/account-overrides.ts +0 -323
- package/src/handlers/account-ownership.ts +0 -25
- package/src/handlers/account-signature.ts +0 -117
- package/src/handlers/account-synced-services.test.ts +0 -245
- package/src/handlers/account.ts +0 -552
- package/src/handlers/address.test.ts +0 -381
- package/src/handlers/address.ts +0 -200
- package/src/handlers/calendar-event.test.ts +0 -685
- package/src/handlers/calendar-event.ts +0 -738
- package/src/handlers/calendar-feed-validators.test.ts +0 -228
- package/src/handlers/calendar-feed.test.ts +0 -439
- package/src/handlers/calendar-feed.ts +0 -291
- package/src/handlers/calendar-sqlite-fixture.ts +0 -36
- package/src/handlers/calendar-suggestion.test.ts +0 -1269
- package/src/handlers/calendar-suggestion.ts +0 -425
- package/src/handlers/calendar.test.ts +0 -1080
- package/src/handlers/calendar.ts +0 -517
- package/src/handlers/config.test.ts +0 -245
- package/src/handlers/config.ts +0 -332
- package/src/handlers/ensure-account-config.ts +0 -30
- package/src/handlers/filter.test.ts +0 -644
- package/src/handlers/filter.ts +0 -518
- package/src/handlers/folder-role-appointments.test.ts +0 -438
- package/src/handlers/folder-role-appointments.ts +0 -249
- package/src/handlers/folder-role-labels.ts +0 -63
- package/src/handlers/folder-role.test.ts +0 -209
- package/src/handlers/folder-role.ts +0 -80
- package/src/handlers/index.ts +0 -83
- package/src/handlers/label.test.ts +0 -112
- package/src/handlers/label.ts +0 -210
- package/src/handlers/mailbox-bindings.ts +0 -111
- package/src/handlers/mailbox-rename-refusal.test.ts +0 -247
- package/src/handlers/mailbox-settled-target.test.ts +0 -251
- package/src/handlers/mailbox.test.ts +0 -280
- package/src/handlers/mailbox.ts +0 -581
- package/src/handlers/me.test.ts +0 -81
- package/src/handlers/me.ts +0 -203
- package/src/handlers/message-read-backfill.test.ts +0 -103
- package/src/handlers/message.test.ts +0 -131
- package/src/handlers/message.ts +0 -1131
- package/src/handlers/organize.test.ts +0 -277
- package/src/handlers/organize.ts +0 -197
- package/src/handlers/outbox-attachment.test.ts +0 -765
- package/src/handlers/outbox-attachment.ts +0 -126
- package/src/handlers/outbox-list.test.ts +0 -179
- package/src/handlers/outbox.test.ts +0 -772
- package/src/handlers/outbox.ts +0 -258
- package/src/handlers/search.test.ts +0 -161
- package/src/handlers/search.ts +0 -105
- package/src/handlers/sync-progress.test.ts +0 -205
- package/src/handlers/sync-progress.ts +0 -99
- package/src/handlers/sync.test.ts +0 -168
- package/src/handlers/sync.ts +0 -118
- package/src/handlers/system-update.test.ts +0 -497
- package/src/handlers/system-update.ts +0 -230
- package/src/handlers/thread.test.ts +0 -396
- package/src/handlers/thread.ts +0 -427
- package/src/handlers/unified-threads-mail-sources.sqlite.test.ts +0 -197
- package/src/handlers/unified-threads.test.ts +0 -785
- package/src/handlers/unified-threads.ts +0 -669
- package/src/index.test.ts +0 -83
- package/src/index.ts +0 -228
- package/src/jwt-auth.test.ts +0 -152
- package/src/jwt-auth.ts +0 -132
- package/src/log-redaction.test.ts +0 -144
- package/src/openapi-spec.test.ts +0 -118
- package/src/request-context.test.ts +0 -61
- package/src/request-context.ts +0 -33
- package/src/request-pipeline.test.ts +0 -88
- package/src/request.ts +0 -10
- package/src/response.test.ts +0 -130
- package/src/response.ts +0 -144
- package/src/service/compose-sqlite.ts +0 -98
- package/src/service/config-import.ts +0 -39
- package/src/service/create-remit-client.ts +0 -521
- package/src/service/data-client.test.ts +0 -97
- package/src/service/data-client.ts +0 -58
- package/src/service/export-identity.ts +0 -39
- package/src/service/filter.ts +0 -40
- package/src/service/fire-and-forget.test.ts +0 -126
- package/src/service/fire-and-forget.ts +0 -79
- package/src/service/localhost-env-config.test.ts +0 -36
- package/src/service/organize-queue.ts +0 -98
- package/src/service/organize.test.ts +0 -1451
- package/src/service/organize.ts +0 -1025
- package/src/service/read-path-filter-config.test.ts +0 -243
- package/src/service/semantic-capability.test.ts +0 -78
- package/src/service/semantic-capability.ts +0 -95
- package/src/service/sender-category.test.ts +0 -235
- package/src/service/trigger-sync.test.ts +0 -205
- package/src/service/trigger-sync.ts +0 -100
- package/src/stdout-capture-fixture.ts +0 -40
- package/src/types.ts +0 -271
- package/tsconfig.json +0 -7
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { buildCalendarFeed, calendarFeedIsUnchanged, calendarFeedIsUnmodifiedSince, hashCalendarFeedToken, isCalendarFeedToken, mintCalendarFeedToken, readCalendarFeedToken, } from "@remit/calendar-service";
|
|
2
|
+
import { NotFoundError } from "@remit/data-ports/errors";
|
|
3
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
4
|
+
import { rawApiResponse } from "../response.js";
|
|
5
|
+
import { getClient } from "../service/data-client.js";
|
|
6
|
+
import { calendarDepsOf, findCalendarFor, notFound, refuseCalendar, } from "./calendar.js";
|
|
7
|
+
const toFeedState = (token) => ({
|
|
8
|
+
calendarId: token.calendarId,
|
|
9
|
+
createdAt: token.createdAt,
|
|
10
|
+
rotatedAt: token.rotatedAt,
|
|
11
|
+
});
|
|
12
|
+
/**
|
|
13
|
+
* Mints the calendar's feed address, or replaces the one it has.
|
|
14
|
+
*
|
|
15
|
+
* The plaintext exists here and nowhere else: the store keeps a digest, so this
|
|
16
|
+
* return value is the only chance anybody has to read the token. Ownership is
|
|
17
|
+
* settled before the mint — a calendar the caller does not hold is absent, not
|
|
18
|
+
* forbidden, which is the same answer `getCalendar` gives it.
|
|
19
|
+
*/
|
|
20
|
+
export const putCalendarFeedFor = async (deps, accountConfigId, calendarId) => {
|
|
21
|
+
const calendar = await findCalendarFor(deps, accountConfigId, calendarId);
|
|
22
|
+
if (!calendar.ok)
|
|
23
|
+
return calendar;
|
|
24
|
+
const secret = mintCalendarFeedToken();
|
|
25
|
+
const stored = await deps.calendarFeedToken.put({
|
|
26
|
+
accountConfigId,
|
|
27
|
+
calendarId,
|
|
28
|
+
tokenHash: secret.tokenHash,
|
|
29
|
+
});
|
|
30
|
+
return {
|
|
31
|
+
ok: true,
|
|
32
|
+
value: {
|
|
33
|
+
calendarId: stored.calendarId,
|
|
34
|
+
feedToken: secret.token,
|
|
35
|
+
createdAt: stored.createdAt,
|
|
36
|
+
rotatedAt: stored.rotatedAt,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
/** Whether the calendar has a feed address, and how old it is. */
|
|
41
|
+
export const findCalendarFeedFor = async (deps, accountConfigId, calendarId) => {
|
|
42
|
+
const calendar = await findCalendarFor(deps, accountConfigId, calendarId);
|
|
43
|
+
if (!calendar.ok)
|
|
44
|
+
return calendar;
|
|
45
|
+
const stored = await deps.calendarFeedToken.findByCalendar(accountConfigId, calendarId);
|
|
46
|
+
if (!stored) {
|
|
47
|
+
return refuseCalendar("NotFound", `no feed on calendar ${calendarId}`);
|
|
48
|
+
}
|
|
49
|
+
return { ok: true, value: toFeedState(stored) };
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Revokes the calendar's feed address.
|
|
53
|
+
*
|
|
54
|
+
* A calendar with no feed is refused rather than answered 204: the caller asked
|
|
55
|
+
* for a subscription to stop working, and reporting success for one that was
|
|
56
|
+
* never there reads as confirmation that it has.
|
|
57
|
+
*/
|
|
58
|
+
export const revokeCalendarFeedFor = async (deps, accountConfigId, calendarId) => {
|
|
59
|
+
const calendar = await findCalendarFor(deps, accountConfigId, calendarId);
|
|
60
|
+
if (!calendar.ok)
|
|
61
|
+
return calendar;
|
|
62
|
+
const stored = await deps.calendarFeedToken.findByCalendar(accountConfigId, calendarId);
|
|
63
|
+
if (!stored) {
|
|
64
|
+
return refuseCalendar("NotFound", `no feed on calendar ${calendarId}`);
|
|
65
|
+
}
|
|
66
|
+
await deps.calendarFeedToken.delete(accountConfigId, calendarId);
|
|
67
|
+
return { ok: true, value: null };
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* No feed answers to this address.
|
|
71
|
+
*
|
|
72
|
+
* One answer for an unknown token, a revoked one and a malformed one, with a
|
|
73
|
+
* body that says the same thing to all three: a caller probing addresses learns
|
|
74
|
+
* only whether the one it sent works, which is what it was going to find out
|
|
75
|
+
* anyway.
|
|
76
|
+
*/
|
|
77
|
+
const feedNotFound = () => rawApiResponse({
|
|
78
|
+
statusCode: 404,
|
|
79
|
+
headers: { "Content-Type": "text/plain; charset=utf-8" },
|
|
80
|
+
body: "no calendar feed at this address\n",
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Serves a calendar to whoever holds its secret address.
|
|
84
|
+
*
|
|
85
|
+
* The token is the whole credential, so a value that cannot be one is refused
|
|
86
|
+
* before anything is hashed, and the digest of what arrived is what the store is
|
|
87
|
+
* asked about — the plaintext is never compared against a stored one because
|
|
88
|
+
* none is stored.
|
|
89
|
+
*
|
|
90
|
+
* `Last-Modified` is the collection's own timestamp, not the newest event in
|
|
91
|
+
* it: an event deleted from a calendar leaves every survivor older than the
|
|
92
|
+
* change, and a validator that moves backwards tells a subscriber its cached
|
|
93
|
+
* copy is still good. The collection is stamped by the same sequence bump every
|
|
94
|
+
* write and every delete goes through, so it only ever moves forward.
|
|
95
|
+
*
|
|
96
|
+
* A row whose collection is gone is the same 404. It should not exist — a
|
|
97
|
+
* calendar's delete takes its token with it, in one batch — and serving a
|
|
98
|
+
* blank calendar for one would be worse than saying nothing.
|
|
99
|
+
*/
|
|
100
|
+
export const serveCalendarFeed = async (deps, feedToken, conditions) => {
|
|
101
|
+
if (!isCalendarFeedToken(feedToken))
|
|
102
|
+
return feedNotFound();
|
|
103
|
+
const stored = await deps.calendarFeedToken.findByTokenHash(hashCalendarFeedToken(feedToken));
|
|
104
|
+
if (!stored)
|
|
105
|
+
return feedNotFound();
|
|
106
|
+
const collection = await deps.calendarCollection
|
|
107
|
+
.get(stored.accountConfigId, stored.calendarId)
|
|
108
|
+
.catch((error) => {
|
|
109
|
+
if (error instanceof NotFoundError)
|
|
110
|
+
return null;
|
|
111
|
+
throw error;
|
|
112
|
+
});
|
|
113
|
+
if (!collection)
|
|
114
|
+
return feedNotFound();
|
|
115
|
+
const objects = await deps.calendarObject.listByCalendar(stored.calendarId);
|
|
116
|
+
const feed = buildCalendarFeed(collection, objects);
|
|
117
|
+
const etag = `"${feed.etag}"`;
|
|
118
|
+
const lastModified = new Date(collection.updatedAt).toUTCString();
|
|
119
|
+
// `If-None-Match` wins outright when both arrive (RFC 9110 13.2.2): the tag
|
|
120
|
+
// is exact where the date is truncated to the second, so honouring the date
|
|
121
|
+
// as well could only turn a 200 the tag asked for into a 304.
|
|
122
|
+
const unchanged = conditions.ifNoneMatch === undefined
|
|
123
|
+
? calendarFeedIsUnmodifiedSince(conditions.ifModifiedSince, collection.updatedAt)
|
|
124
|
+
: calendarFeedIsUnchanged(conditions.ifNoneMatch, feed.etag);
|
|
125
|
+
if (unchanged) {
|
|
126
|
+
return rawApiResponse({
|
|
127
|
+
statusCode: 304,
|
|
128
|
+
headers: { ETag: etag, "Last-Modified": lastModified },
|
|
129
|
+
body: "",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return rawApiResponse({
|
|
133
|
+
statusCode: 200,
|
|
134
|
+
headers: {
|
|
135
|
+
"Content-Type": "text/calendar; charset=utf-8",
|
|
136
|
+
ETag: etag,
|
|
137
|
+
"Last-Modified": lastModified,
|
|
138
|
+
},
|
|
139
|
+
body: feed.icalData,
|
|
140
|
+
});
|
|
141
|
+
};
|
|
142
|
+
export const CalendarFeedDetailOperations = {
|
|
143
|
+
CalendarDetailOperations_getCalendarFeed: async (context, ...args) => {
|
|
144
|
+
const event = args[0];
|
|
145
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
146
|
+
const { calendarId } = context.request.params;
|
|
147
|
+
const deps = calendarDepsOf(await getClient());
|
|
148
|
+
const found = await findCalendarFeedFor(deps, accountConfigId, calendarId);
|
|
149
|
+
if (!found.ok)
|
|
150
|
+
return notFound(found.error.message);
|
|
151
|
+
return found.value;
|
|
152
|
+
},
|
|
153
|
+
CalendarDetailOperations_putCalendarFeed: async (context, ...args) => {
|
|
154
|
+
const event = args[0];
|
|
155
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
156
|
+
const { calendarId } = context.request.params;
|
|
157
|
+
const deps = calendarDepsOf(await getClient());
|
|
158
|
+
const minted = await putCalendarFeedFor(deps, accountConfigId, calendarId);
|
|
159
|
+
if (!minted.ok)
|
|
160
|
+
return notFound(minted.error.message);
|
|
161
|
+
return minted.value;
|
|
162
|
+
},
|
|
163
|
+
CalendarDetailOperations_revokeCalendarFeed: async (context, ...args) => {
|
|
164
|
+
const event = args[0];
|
|
165
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
166
|
+
const { calendarId } = context.request.params;
|
|
167
|
+
const deps = calendarDepsOf(await getClient());
|
|
168
|
+
const revoked = await revokeCalendarFeedFor(deps, accountConfigId, calendarId);
|
|
169
|
+
if (!revoked.ok)
|
|
170
|
+
return notFound(revoked.error.message);
|
|
171
|
+
return { statusCode: 204 };
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
export const CalendarFeedOperations = {
|
|
175
|
+
CalendarFeedOperations_getCalendarFeedIcal: async (context, ...args) => {
|
|
176
|
+
// From the raw path, not from the router's captured parameter: the matcher
|
|
177
|
+
// substitutes the parameter into the literal path and leaves the dot of
|
|
178
|
+
// ".ics" as a regex dot, so /feeds/calendar/<token>Xics routes here too —
|
|
179
|
+
// and answering it would serve the feed at an address nobody was handed.
|
|
180
|
+
const event = args[0];
|
|
181
|
+
const feedToken = readCalendarFeedToken(event.path);
|
|
182
|
+
if (feedToken === null)
|
|
183
|
+
return feedNotFound();
|
|
184
|
+
const ifNoneMatch = context.request.headers["if-none-match"];
|
|
185
|
+
const ifModifiedSince = context.request.headers["if-modified-since"];
|
|
186
|
+
const deps = calendarDepsOf(await getClient());
|
|
187
|
+
return serveCalendarFeed(deps, feedToken, {
|
|
188
|
+
ifNoneMatch: typeof ifNoneMatch === "string" ? ifNoneMatch : undefined,
|
|
189
|
+
ifModifiedSince: typeof ifModifiedSince === "string" ? ifModifiedSince : undefined,
|
|
190
|
+
});
|
|
191
|
+
},
|
|
192
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { CalendarSuggestionResponse } from "@remit/api-openapi-types";
|
|
2
|
+
import { type CalendarSuggestionItem, type ICalendarSuggestionRepository, type IEnvelopeRepository, type IFilterRepository, type IThreadMessageRepository } from "@remit/data-ports";
|
|
3
|
+
import type { CalendarSuggestionActionOperationIds, CalendarSuggestionOperationIds, MessageCalendarSuggestionOperationIds, OperationHandler } from "../types.js";
|
|
4
|
+
/**
|
|
5
|
+
* The raw invitation bytes stay on the server. A client renders the projected
|
|
6
|
+
* fields; the bytes exist so accepting can write them into a calendar
|
|
7
|
+
* unchanged, and shipping them would invite a second, divergent renderer.
|
|
8
|
+
*/
|
|
9
|
+
export declare const toCalendarSuggestionResponse: (item: CalendarSuggestionItem, answerOvertakenBy: CalendarSuggestionResponse["answerOvertakenBy"]) => CalendarSuggestionResponse;
|
|
10
|
+
export declare const toCalendarSuggestionResponses: (deps: {
|
|
11
|
+
threadMessage: Pick<IThreadMessageRepository, "findByMessageId">;
|
|
12
|
+
calendarSuggestion: Pick<ICalendarSuggestionRepository, "listByAcceptedCalendarObjects">;
|
|
13
|
+
}, accountConfigId: string, items: CalendarSuggestionItem[]) => Promise<CalendarSuggestionResponse[]>;
|
|
14
|
+
/**
|
|
15
|
+
* The states a person can move a pending card into. `Superseded` is the
|
|
16
|
+
* producer's alone — a revision retires a card, a person never does — and the
|
|
17
|
+
* rest are terminal.
|
|
18
|
+
*/
|
|
19
|
+
export declare const assertSettleable: (suggestion: CalendarSuggestionItem, target: CalendarSuggestionItem["state"]) => void;
|
|
20
|
+
/** What muting a sender needs, so it can be driven without a live table. */
|
|
21
|
+
export interface MuteSenderDeps {
|
|
22
|
+
envelope: Pick<IEnvelopeRepository, "getMessageData">;
|
|
23
|
+
filter: Pick<IFilterRepository, "create" | "listByAccountAndState">;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The standing rule `dismiss{muteSender:true}` writes: the user saying they do
|
|
27
|
+
* not want this sender's invitations offered. It goes through the existing
|
|
28
|
+
* `Filter` entity rather than a second rules table, so it is visible and
|
|
29
|
+
* editable beside every other rule the user has, and matches on the message's
|
|
30
|
+
* From address rather than on the invitation's ORGANIZER — a `PUBLISH` names
|
|
31
|
+
* no organizer at all, and the sender is who the user is refusing.
|
|
32
|
+
*
|
|
33
|
+
* At most one rule per sender. Dismissing the same card twice, or muting a
|
|
34
|
+
* sender already muted from another card, is the same instruction repeated —
|
|
35
|
+
* a second identical rule would only be a second row for the settings page to
|
|
36
|
+
* show and the user to delete twice.
|
|
37
|
+
*/
|
|
38
|
+
export declare const muteSender: (deps: MuteSenderDeps, accountConfigId: string, messageId: string) => Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Moves a card to a decision. Idempotent by design: a client that retries a
|
|
41
|
+
* decline gets the same answer rather than a 400, and only a card in a
|
|
42
|
+
* different terminal state is refused.
|
|
43
|
+
*/
|
|
44
|
+
export declare const settleSuggestion: (repo: ICalendarSuggestionRepository, accountConfigId: string, suggestionId: string, state: CalendarSuggestionItem["state"]) => Promise<CalendarSuggestionItem>;
|
|
45
|
+
export declare const CalendarSuggestionOperations: Record<CalendarSuggestionOperationIds, OperationHandler<CalendarSuggestionOperationIds>>;
|
|
46
|
+
export declare const MessageCalendarSuggestionOperations: Record<MessageCalendarSuggestionOperationIds, OperationHandler<MessageCalendarSuggestionOperationIds>>;
|
|
47
|
+
export declare const CalendarSuggestionActionOperations: Record<CalendarSuggestionActionOperationIds, OperationHandler<CalendarSuggestionActionOperationIds>>;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { acceptCalendarSuggestion, answersOvertakenBy, correctCalendarSuggestion, reopenCalendarSuggestion, } from "@remit/calendar-service";
|
|
2
|
+
import { isSenderMuted, } from "@remit/data-ports";
|
|
3
|
+
import { BadRequestError } from "@remit/data-ports/errors";
|
|
4
|
+
import { CalendarInviteAnswer, CalendarInviteMethod, CalendarSource, CalendarSuggestionState, FilterClauseField, FilterMatchOperator, FilterScope, FilterState, } from "@remit/domain-enums";
|
|
5
|
+
import { getAccountConfigIdFromEvent } from "../auth.js";
|
|
6
|
+
import { getClient } from "../service/data-client.js";
|
|
7
|
+
import { pickEventUpdate } from "./calendar-event.js";
|
|
8
|
+
/**
|
|
9
|
+
* The raw invitation bytes stay on the server. A client renders the projected
|
|
10
|
+
* fields; the bytes exist so accepting can write them into a calendar
|
|
11
|
+
* unchanged, and shipping them would invite a second, divergent renderer.
|
|
12
|
+
*/
|
|
13
|
+
export const toCalendarSuggestionResponse = (item, answerOvertakenBy) => {
|
|
14
|
+
const { icalData: _icalData, ...response } = item;
|
|
15
|
+
return { ...response, supersededByThreadId: "", answerOvertakenBy };
|
|
16
|
+
};
|
|
17
|
+
export const toCalendarSuggestionResponses = async (deps, accountConfigId, items) => Promise.all((await answersOvertakenBy(deps.calendarSuggestion, accountConfigId, items)).map(async ({ suggestion, answerOvertakenBy }) => {
|
|
18
|
+
const response = toCalendarSuggestionResponse(suggestion, answerOvertakenBy);
|
|
19
|
+
if (suggestion.supersededByMessageId === "")
|
|
20
|
+
return response;
|
|
21
|
+
const newer = await deps.threadMessage.findByMessageId(accountConfigId, suggestion.supersededByMessageId);
|
|
22
|
+
return { ...response, supersededByThreadId: newer?.threadId ?? "" };
|
|
23
|
+
}));
|
|
24
|
+
const respondOne = async (client, accountConfigId, item) => {
|
|
25
|
+
const [response] = await toCalendarSuggestionResponses(client, accountConfigId, [item]);
|
|
26
|
+
if (!response)
|
|
27
|
+
throw new Error("unreachable: one item in, one response out");
|
|
28
|
+
return response;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* The states a person can move a pending card into. `Superseded` is the
|
|
32
|
+
* producer's alone — a revision retires a card, a person never does — and the
|
|
33
|
+
* rest are terminal.
|
|
34
|
+
*/
|
|
35
|
+
export const assertSettleable = (suggestion, target) => {
|
|
36
|
+
if (suggestion.state === CalendarSuggestionState.Pending)
|
|
37
|
+
return;
|
|
38
|
+
if (suggestion.state === target)
|
|
39
|
+
return;
|
|
40
|
+
throw new BadRequestError(`This suggestion was already ${suggestion.state.toLowerCase()}, so it can't be ${target.toLowerCase()}.`);
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The mail address the card's event should name as the accepting attendee: the
|
|
44
|
+
* account the invitation arrived on. Resolved from the message rather than
|
|
45
|
+
* asked of the client, so an ATTENDEE line can never name an address the user
|
|
46
|
+
* does not own.
|
|
47
|
+
*/
|
|
48
|
+
const accountEmailForMessage = async (client, messageId) => {
|
|
49
|
+
const message = await client.message.get(messageId);
|
|
50
|
+
const accountId = await client.mailbox.resolveAccountId(message.mailboxId);
|
|
51
|
+
if (!accountId) {
|
|
52
|
+
throw new BadRequestError("The folder this invitation arrived in is gone, so there is no address to accept as.");
|
|
53
|
+
}
|
|
54
|
+
const account = await client.account.get(accountId);
|
|
55
|
+
return account.email;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The standing rule `dismiss{muteSender:true}` writes: the user saying they do
|
|
59
|
+
* not want this sender's invitations offered. It goes through the existing
|
|
60
|
+
* `Filter` entity rather than a second rules table, so it is visible and
|
|
61
|
+
* editable beside every other rule the user has, and matches on the message's
|
|
62
|
+
* From address rather than on the invitation's ORGANIZER — a `PUBLISH` names
|
|
63
|
+
* no organizer at all, and the sender is who the user is refusing.
|
|
64
|
+
*
|
|
65
|
+
* At most one rule per sender. Dismissing the same card twice, or muting a
|
|
66
|
+
* sender already muted from another card, is the same instruction repeated —
|
|
67
|
+
* a second identical rule would only be a second row for the settings page to
|
|
68
|
+
* show and the user to delete twice.
|
|
69
|
+
*/
|
|
70
|
+
export const muteSender = async (deps, accountConfigId, messageId) => {
|
|
71
|
+
const data = await deps.envelope.getMessageData(messageId);
|
|
72
|
+
const from = data.envelopeAddress.find((address) => address.addressRole === "from");
|
|
73
|
+
if (!from) {
|
|
74
|
+
throw new BadRequestError("This message names no sender, so there is nobody to mute.");
|
|
75
|
+
}
|
|
76
|
+
const active = await deps.filter.listByAccountAndState(accountConfigId, FilterState.Active);
|
|
77
|
+
if (isSenderMuted(active, from.normalizedEmail))
|
|
78
|
+
return;
|
|
79
|
+
await deps.filter.create({
|
|
80
|
+
accountConfigId,
|
|
81
|
+
name: `Muted invitations from ${from.normalizedEmail}`,
|
|
82
|
+
scope: FilterScope.Standing,
|
|
83
|
+
matchOperator: FilterMatchOperator.And,
|
|
84
|
+
literalClauses: [
|
|
85
|
+
{ field: FilterClauseField.From, value: from.normalizedEmail },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Moves a card to a decision. Idempotent by design: a client that retries a
|
|
91
|
+
* decline gets the same answer rather than a 400, and only a card in a
|
|
92
|
+
* different terminal state is refused.
|
|
93
|
+
*/
|
|
94
|
+
export const settleSuggestion = async (repo, accountConfigId, suggestionId, state) => {
|
|
95
|
+
const suggestion = await repo.get(accountConfigId, suggestionId);
|
|
96
|
+
assertSettleable(suggestion, state);
|
|
97
|
+
if (suggestion.state === state)
|
|
98
|
+
return suggestion;
|
|
99
|
+
return repo.settle(accountConfigId, suggestionId, {
|
|
100
|
+
state,
|
|
101
|
+
acceptedCalendarObjectId: "",
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
export const CalendarSuggestionOperations = {
|
|
105
|
+
CalendarSuggestionOperations_listCalendarSuggestions: async (context, ...args) => {
|
|
106
|
+
const event = args[0];
|
|
107
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
108
|
+
const { state, continuationToken } = context.request.query;
|
|
109
|
+
const client = await getClient();
|
|
110
|
+
const page = await client.calendarSuggestion.listByState(accountConfigId, state, {
|
|
111
|
+
continuationToken,
|
|
112
|
+
endsAfter: state === CalendarSuggestionState.Pending
|
|
113
|
+
? new Date().toISOString()
|
|
114
|
+
: new Date(0).toISOString(),
|
|
115
|
+
});
|
|
116
|
+
return {
|
|
117
|
+
items: await toCalendarSuggestionResponses(client, accountConfigId, page.items),
|
|
118
|
+
continuationToken: page.continuationToken,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
export const MessageCalendarSuggestionOperations = {
|
|
123
|
+
MessageCalendarSuggestionOperations_listMessageCalendarSuggestions: async (context, ...args) => {
|
|
124
|
+
const event = args[0];
|
|
125
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
126
|
+
const { messageId } = context.request.params;
|
|
127
|
+
const client = await getClient();
|
|
128
|
+
const items = await client.calendarSuggestion.listByMessage(accountConfigId, messageId);
|
|
129
|
+
return {
|
|
130
|
+
items: await toCalendarSuggestionResponses(client, accountConfigId, items),
|
|
131
|
+
continuationToken: undefined,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
export const CalendarSuggestionActionOperations = {
|
|
136
|
+
CalendarSuggestionActionOperations_acceptCalendarSuggestion: async (context, ...args) => {
|
|
137
|
+
const event = args[0];
|
|
138
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
139
|
+
const { suggestionId } = context.request.params;
|
|
140
|
+
const { calendarId, answer = CalendarInviteAnswer.Accepted } = context
|
|
141
|
+
.request.requestBody;
|
|
142
|
+
const client = await getClient();
|
|
143
|
+
const suggestion = await client.calendarSuggestion.get(accountConfigId, suggestionId);
|
|
144
|
+
if (answer === CalendarInviteAnswer.Tentative &&
|
|
145
|
+
suggestion.method === CalendarInviteMethod.Cancel) {
|
|
146
|
+
throw new BadRequestError("This is a cancellation, so there is nothing to answer maybe to.");
|
|
147
|
+
}
|
|
148
|
+
assertSettleable(suggestion, answer === CalendarInviteAnswer.Tentative
|
|
149
|
+
? CalendarSuggestionState.Tentative
|
|
150
|
+
: CalendarSuggestionState.Accepted);
|
|
151
|
+
// Reads the collection through the caller's own account config, so a
|
|
152
|
+
// calendarId naming somebody else's collection is a 404 before anything
|
|
153
|
+
// is written into it.
|
|
154
|
+
const collection = await client.calendarCollection.get(accountConfigId, calendarId);
|
|
155
|
+
if (collection.source === CalendarSource.Subscribed) {
|
|
156
|
+
throw new BadRequestError(`"${collection.displayName}" is subscribed to a feed and is read-only — accept the invitation into another calendar`);
|
|
157
|
+
}
|
|
158
|
+
const { event: corrections } = context.request.requestBody;
|
|
159
|
+
const corrected = corrections === undefined
|
|
160
|
+
? { ok: true, value: suggestion }
|
|
161
|
+
: await correctCalendarSuggestion(suggestion, pickEventUpdate(corrections), collection.timezone);
|
|
162
|
+
if (!corrected.ok) {
|
|
163
|
+
throw new BadRequestError(corrected.error.message);
|
|
164
|
+
}
|
|
165
|
+
const accepted = await acceptCalendarSuggestion(client.calendarStore, {
|
|
166
|
+
accountConfigId,
|
|
167
|
+
calendarId,
|
|
168
|
+
suggestion: corrected.value,
|
|
169
|
+
attendee: await accountEmailForMessage(client, suggestion.messageId),
|
|
170
|
+
answer,
|
|
171
|
+
});
|
|
172
|
+
if (!accepted.ok) {
|
|
173
|
+
throw new BadRequestError(accepted.error.message);
|
|
174
|
+
}
|
|
175
|
+
return respondOne(client, accountConfigId, accepted.value.suggestion);
|
|
176
|
+
},
|
|
177
|
+
CalendarSuggestionActionOperations_declineCalendarSuggestion: async (context, ...args) => {
|
|
178
|
+
const event = args[0];
|
|
179
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
180
|
+
const { suggestionId } = context.request.params;
|
|
181
|
+
const client = await getClient();
|
|
182
|
+
const declined = await settleSuggestion(client.calendarSuggestion, accountConfigId, suggestionId, CalendarSuggestionState.Declined);
|
|
183
|
+
return respondOne(client, accountConfigId, declined);
|
|
184
|
+
},
|
|
185
|
+
CalendarSuggestionActionOperations_dismissCalendarSuggestion: async (context, ...args) => {
|
|
186
|
+
const event = args[0];
|
|
187
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
188
|
+
const { suggestionId } = context.request.params;
|
|
189
|
+
const body = (context.request.requestBody ?? {});
|
|
190
|
+
const client = await getClient();
|
|
191
|
+
const dismissed = await settleSuggestion(client.calendarSuggestion, accountConfigId, suggestionId, CalendarSuggestionState.Dismissed);
|
|
192
|
+
if (body.muteSender) {
|
|
193
|
+
await muteSender(client, accountConfigId, dismissed.messageId);
|
|
194
|
+
}
|
|
195
|
+
return respondOne(client, accountConfigId, dismissed);
|
|
196
|
+
},
|
|
197
|
+
CalendarSuggestionActionOperations_reopenCalendarSuggestion: async (context, ...args) => {
|
|
198
|
+
const event = args[0];
|
|
199
|
+
const accountConfigId = getAccountConfigIdFromEvent(event);
|
|
200
|
+
const { suggestionId } = context.request.params;
|
|
201
|
+
const client = await getClient();
|
|
202
|
+
const suggestion = await client.calendarSuggestion.get(accountConfigId, suggestionId);
|
|
203
|
+
const reopened = await reopenCalendarSuggestion(client.calendarStore, {
|
|
204
|
+
accountConfigId,
|
|
205
|
+
suggestion,
|
|
206
|
+
});
|
|
207
|
+
if (!reopened.ok) {
|
|
208
|
+
throw new BadRequestError(reopened.error.message);
|
|
209
|
+
}
|
|
210
|
+
return respondOne(client, accountConfigId, reopened.value);
|
|
211
|
+
},
|
|
212
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { CreateCalendarInput, UpdateCalendarInput } from "@remit/api-openapi-types";
|
|
2
|
+
import { type CalendarFeedFetcher } from "@remit/calendar-service";
|
|
3
|
+
import type { CalendarCollectionItem, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, UpdateCalendarCollectionInput } from "@remit/data-ports";
|
|
4
|
+
import type { RemitClient } from "../service/data-client.js";
|
|
5
|
+
import type { CalendarCollectionDetailOperationIds, CalendarDetailOperationIds, CalendarOperationIds, OperationHandler } from "../types.js";
|
|
6
|
+
/** The calendar store, as the handlers reach it. */
|
|
7
|
+
export interface CalendarDeps {
|
|
8
|
+
calendarCollection: ICalendarCollectionRepository;
|
|
9
|
+
calendarObject: ICalendarObjectRepository;
|
|
10
|
+
calendarEventIndex: ICalendarEventIndexRepository;
|
|
11
|
+
calendarFeedToken: ICalendarFeedTokenRepository;
|
|
12
|
+
calendarStore: ICalendarStore;
|
|
13
|
+
}
|
|
14
|
+
/** Why a calendar request was refused, in a form a client can branch on. */
|
|
15
|
+
export interface CalendarRefusal {
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A refusal is a value here, not a throw. Every one of these is something the
|
|
21
|
+
* caller sent — a window that runs backwards, a segment already taken, a
|
|
22
|
+
* RECURRENCE-ID naming no occurrence — so it is an ordinary outcome of the
|
|
23
|
+
* request rather than a fault of the server's.
|
|
24
|
+
*/
|
|
25
|
+
export type CalendarOutcome<T> = {
|
|
26
|
+
ok: true;
|
|
27
|
+
value: T;
|
|
28
|
+
} | {
|
|
29
|
+
ok: false;
|
|
30
|
+
error: CalendarRefusal;
|
|
31
|
+
};
|
|
32
|
+
export declare const refuseCalendar: <T>(code: string, message: string) => CalendarOutcome<T>;
|
|
33
|
+
/**
|
|
34
|
+
* A 400 keeps the refusal's own code — `unknown_time_zone` and
|
|
35
|
+
* `url_segment_taken` are what a client branches on, and the status class alone
|
|
36
|
+
* would not distinguish them. A 404 and a 412 have nothing to add to their
|
|
37
|
+
* status, so they take the shared token.
|
|
38
|
+
*/
|
|
39
|
+
export declare const badRequest: (error: CalendarRefusal) => {
|
|
40
|
+
statusCode: number;
|
|
41
|
+
body: {
|
|
42
|
+
code: string;
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
export declare const notFound: (message: string) => {
|
|
47
|
+
statusCode: number;
|
|
48
|
+
body: {
|
|
49
|
+
code: string;
|
|
50
|
+
message: string;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
export declare const preconditionFailed: (message: string) => {
|
|
54
|
+
statusCode: number;
|
|
55
|
+
body: {
|
|
56
|
+
code: string;
|
|
57
|
+
message: string;
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
/** The calendar half of the client, named so a handler takes only what it uses. */
|
|
61
|
+
export declare const calendarDepsOf: (client: RemitClient) => CalendarDeps;
|
|
62
|
+
/**
|
|
63
|
+
* Every collection the account config holds, provisioning the default one when
|
|
64
|
+
* it holds none.
|
|
65
|
+
*
|
|
66
|
+
* Safe to race with itself: `calendarId` is derived from the account config and
|
|
67
|
+
* the URL segment, so two first reads arriving together write the same row
|
|
68
|
+
* rather than two, and the loser's write is a no-op instead of a second
|
|
69
|
+
* calendar nobody asked for.
|
|
70
|
+
*/
|
|
71
|
+
export declare const listCalendarsFor: (deps: CalendarDeps, accountConfigId: string) => Promise<CalendarCollectionItem[]>;
|
|
72
|
+
/** One of the caller's collections, or a refusal naming the one they asked for. */
|
|
73
|
+
export declare const findCalendarFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string) => Promise<CalendarOutcome<CalendarCollectionItem>>;
|
|
74
|
+
/**
|
|
75
|
+
* One of the caller's collections that a person may write events into. A
|
|
76
|
+
* subscribed collection holds what its feed holds: an edit made here would be
|
|
77
|
+
* overwritten by the next refresh, so it is refused rather than accepted and
|
|
78
|
+
* silently lost.
|
|
79
|
+
*/
|
|
80
|
+
export declare const findWritableCalendarFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string) => Promise<CalendarOutcome<CalendarCollectionItem>>;
|
|
81
|
+
/**
|
|
82
|
+
* A collection's timezone is what every floating time in it is read in, so a
|
|
83
|
+
* name this server cannot resolve is not a cosmetic setting — it silently moves
|
|
84
|
+
* every all-day and unzoned event in the calendar. A Windows zone name, which
|
|
85
|
+
* is what a client that has not normalised its input sends, is refused here
|
|
86
|
+
* rather than stored and quietly read as UTC.
|
|
87
|
+
*/
|
|
88
|
+
export declare const readCollectionTimezone: (timezone: string | undefined) => CalendarOutcome<string>;
|
|
89
|
+
export declare const createCalendarFor: (deps: CalendarDeps, accountConfigId: string, input: CreateCalendarInput, fetcher?: CalendarFeedFetcher, now?: number) => Promise<CalendarOutcome<CalendarCollectionItem>>;
|
|
90
|
+
/**
|
|
91
|
+
* Removes a collection with everything in it, in one unit.
|
|
92
|
+
*
|
|
93
|
+
* The default collection stays: it is where an accepted invitation and a first
|
|
94
|
+
* event land, and an account config with no calendar has nowhere to put one.
|
|
95
|
+
*/
|
|
96
|
+
export declare const deleteCalendarFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string) => Promise<CalendarOutcome<null>>;
|
|
97
|
+
/**
|
|
98
|
+
* Reduce a PATCH body to the fields a collection update may set. `urlSegment`
|
|
99
|
+
* is deliberately absent: it is the collection's identity and the path a client
|
|
100
|
+
* has bookmarked, so moving it is making a different calendar.
|
|
101
|
+
*/
|
|
102
|
+
export declare const pickCalendarUpdate: (body: Partial<UpdateCalendarInput>) => UpdateCalendarCollectionInput;
|
|
103
|
+
/**
|
|
104
|
+
* Applies a collection patch, rewriting what the collection's timezone decides.
|
|
105
|
+
*
|
|
106
|
+
* The timezone is not a label. Every floating and all-day time in the
|
|
107
|
+
* collection is read in it, so changing it moves every occurrence row those
|
|
108
|
+
* resources produced — and a row left at the old zone is an event drawn hours
|
|
109
|
+
* from where the calendar now says it is. Each resource is therefore written
|
|
110
|
+
* again from its own stored bytes, which re-projects and re-expands it and
|
|
111
|
+
* bumps the collection's sequence so a syncing client sees the change. The
|
|
112
|
+
* whole set is one unit: a half-converted calendar is worse than either zone.
|
|
113
|
+
*/
|
|
114
|
+
export declare const updateCalendarFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string, body: Partial<UpdateCalendarInput>) => Promise<CalendarOutcome<CalendarCollectionItem>>;
|
|
115
|
+
export declare const CalendarOperations: Record<CalendarOperationIds, OperationHandler<CalendarOperationIds>>;
|
|
116
|
+
export declare const CalendarDetailOperations: Record<CalendarCollectionDetailOperationIds, OperationHandler<CalendarDetailOperationIds>>;
|