@remit/backend 0.0.156 → 1.0.0
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/src/handlers/calendar-event.d.ts +58 -12
- package/dist/src/handlers/calendar-event.js +291 -100
- package/dist/src/handlers/calendar-suggestion.js +3 -0
- package/dist/src/handlers/calendar-task.d.ts +17 -0
- package/dist/src/handlers/calendar-task.js +175 -0
- package/dist/src/handlers/calendar.d.ts +6 -1
- package/dist/src/handlers/calendar.js +18 -4
- package/dist/src/handlers/index.d.ts +13 -5
- package/dist/src/handlers/index.js +5 -0
- package/dist/src/handlers/user-settings.d.ts +22 -0
- package/dist/src/handlers/user-settings.js +55 -0
- package/dist/src/service/create-remit-client.d.ts +5 -1
- package/dist/src/service/create-remit-client.js +3 -0
- package/dist/src/types.d.ts +4 -1
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { CreateCalendarEventInput, UpdateCalendarEventInput } from "@remit/api-openapi-types";
|
|
2
|
-
import { type CalendarEventFields, type CalendarWindow, type RecurrenceScopeValue, type ScopedWrite } from "@remit/calendar-service";
|
|
3
|
-
import type { CalendarCollectionItem, CalendarObjectItem } from "@remit/data-ports";
|
|
1
|
+
import type { CalendarEventResponse, CalendarSchedulingSend, CreateCalendarEventInput, UpdateCalendarEventInput } from "@remit/api-openapi-types";
|
|
2
|
+
import { type CalendarEventFields, type CalendarWindow, type EventSchedulingUpdate, type RecurrenceScopeValue, type ScopedWrite } from "@remit/calendar-service";
|
|
3
|
+
import type { CalendarCollectionItem, CalendarObjectItem, CreateOutboxSchedulingPartInput, OutboxMessageItem } from "@remit/data-ports";
|
|
4
|
+
import type { CreateDraftInput } from "@remit/mailbox-service";
|
|
4
5
|
import type { CalendarEventDetailOperationIds, CalendarEventOperationIds, CalendarFreeBusyOperationIds, OperationHandler } from "../types.js";
|
|
5
6
|
import { type CalendarDeps, type CalendarOutcome } from "./calendar.js";
|
|
6
7
|
/**
|
|
@@ -9,12 +10,15 @@ import { type CalendarDeps, type CalendarOutcome } from "./calendar.js";
|
|
|
9
10
|
* the server to expand a decade of a daily series.
|
|
10
11
|
*/
|
|
11
12
|
export declare const CALENDAR_MAX_WINDOW_DAYS = 366;
|
|
12
|
-
/**
|
|
13
|
+
/** Queues one scheduling message with its part; a failed enqueue answers with the failed row. */
|
|
14
|
+
export type QueueScheduled = (input: CreateDraftInput, part: CreateOutboxSchedulingPartInput) => Promise<OutboxMessageItem>;
|
|
15
|
+
/** What an event write needs beyond the reads: ids, a clock, and the outbox. */
|
|
13
16
|
export interface CalendarEventDeps extends CalendarDeps {
|
|
14
17
|
newId: () => string;
|
|
15
18
|
now: () => Date;
|
|
19
|
+
queueScheduled: QueueScheduled;
|
|
16
20
|
}
|
|
17
|
-
export declare const calendarEventDepsOf: (deps: CalendarDeps) => CalendarEventDeps;
|
|
21
|
+
export declare const calendarEventDepsOf: (deps: CalendarDeps, queueScheduled: QueueScheduled) => CalendarEventDeps;
|
|
18
22
|
/**
|
|
19
23
|
* Reads the window a listing asked for.
|
|
20
24
|
*
|
|
@@ -23,14 +27,23 @@ export declare const calendarEventDepsOf: (deps: CalendarDeps) => CalendarEventD
|
|
|
23
27
|
* apparently empty morning rather than an error.
|
|
24
28
|
*/
|
|
25
29
|
export declare const readWindow: (from: unknown, to: unknown) => CalendarOutcome<CalendarWindow>;
|
|
26
|
-
/**
|
|
27
|
-
|
|
30
|
+
/**
|
|
31
|
+
* The collections a listing covers, defaulting to every one the caller holds
|
|
32
|
+
* of the component set asked for.
|
|
33
|
+
*/
|
|
34
|
+
export declare const collectionsForListing: (deps: CalendarDeps, accountConfigId: string, calendarIds: string[], componentSet?: CalendarCollectionItem["componentSet"]) => Promise<CalendarOutcome<CalendarCollectionItem[]>>;
|
|
35
|
+
/**
|
|
36
|
+
* An event as the API returns it: the stored row, plus the guest list read
|
|
37
|
+
* from its bytes and the reader's own place on it.
|
|
38
|
+
*/
|
|
39
|
+
export declare const toEventResponse: (object: CalendarObjectItem, self: ReadonlySet<string>, sends: CalendarSchedulingSend[]) => Promise<CalendarEventResponse>;
|
|
28
40
|
/**
|
|
29
41
|
* Reduce a PATCH body to the event fields an update may set, preserving
|
|
30
42
|
* absence: a body carrying only `summary` yields only `summary`, and the
|
|
31
43
|
* resource's times are then left exactly as they were.
|
|
32
44
|
*/
|
|
33
45
|
export declare const pickEventUpdate: (body: Partial<UpdateCalendarEventInput>) => Partial<CalendarEventFields>;
|
|
46
|
+
export declare const pickSchedulingUpdate: (body: Partial<UpdateCalendarEventInput>) => EventSchedulingUpdate;
|
|
34
47
|
/**
|
|
35
48
|
* Whether an `If-Match` header lets the write through.
|
|
36
49
|
*
|
|
@@ -55,7 +68,12 @@ export declare const readScope: (value: unknown) => CalendarOutcome<RecurrenceSc
|
|
|
55
68
|
* user sees twice or not at all.
|
|
56
69
|
*/
|
|
57
70
|
export declare const commitScopedWrite: (deps: CalendarEventDeps, accountConfigId: string, object: CalendarObjectItem, collectionTimezone: string, write: ScopedWrite) => Promise<CalendarOutcome<CalendarObjectItem | null>>;
|
|
58
|
-
|
|
71
|
+
/** An event write, and the scheduling messages it queued. */
|
|
72
|
+
export interface ScheduledWrite<T> {
|
|
73
|
+
object: T;
|
|
74
|
+
sends: CalendarSchedulingSend[];
|
|
75
|
+
}
|
|
76
|
+
export declare const createCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, input: CreateCalendarEventInput) => Promise<CalendarOutcome<ScheduledWrite<CalendarObjectItem>>>;
|
|
59
77
|
export interface ScopedRequest {
|
|
60
78
|
calendarId: string;
|
|
61
79
|
calendarObjectId: string;
|
|
@@ -63,8 +81,36 @@ export interface ScopedRequest {
|
|
|
63
81
|
recurrenceId: string;
|
|
64
82
|
ifMatch: string | undefined;
|
|
65
83
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Edits an event, and queues what the edit owes its guests.
|
|
86
|
+
*
|
|
87
|
+
* The calendar row is local truth and is written first. The outbox rows are
|
|
88
|
+
* the pending records of the messages that follow it: an invitation to every
|
|
89
|
+
* guest of an event the reader organizes, a cancellation to a guest the edit
|
|
90
|
+
* removed, a reply to the organizer when the reader answers as a guest. The
|
|
91
|
+
* outbox lives in another database and is not written in the calendar's
|
|
92
|
+
* commit; a later edit made while an earlier message is still queued does not
|
|
93
|
+
* wait for it, and each message carries the SEQUENCE of the bytes it was built
|
|
94
|
+
* from, so a recipient keeps the newest.
|
|
95
|
+
*/
|
|
96
|
+
export declare const updateCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, request: ScopedRequest, patch: Partial<CalendarEventFields>, scheduling?: EventSchedulingUpdate) => Promise<CalendarOutcome<ScheduledWrite<CalendarObjectItem | null>>>;
|
|
97
|
+
/**
|
|
98
|
+
* Deletes an event, or one occurrence of it, and queues what that owes its
|
|
99
|
+
* guests: a cancellation each when the whole event goes, the shortened event
|
|
100
|
+
* when part of a series does. Only an event the reader organizes owes anything.
|
|
101
|
+
*/
|
|
102
|
+
export declare const deleteCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, request: ScopedRequest) => Promise<CalendarOutcome<ScheduledWrite<CalendarObjectItem | null>>>;
|
|
103
|
+
export declare const answerRefusal: (error: {
|
|
104
|
+
code: string;
|
|
105
|
+
message: string;
|
|
106
|
+
}) => {
|
|
107
|
+
statusCode: number;
|
|
108
|
+
body: {
|
|
109
|
+
code: string;
|
|
110
|
+
message: string;
|
|
111
|
+
};
|
|
112
|
+
};
|
|
113
|
+
export declare const readIfMatch: (context: Parameters<OperationHandler>[0]) => string | undefined;
|
|
114
|
+
export declare const CalendarEventOperations: Record<CalendarEventOperationIds, OperationHandler<CalendarEventOperationIds, "smtp">>;
|
|
115
|
+
export declare const CalendarEventDetailOperations: Record<CalendarEventDetailOperationIds, OperationHandler<CalendarEventDetailOperationIds, "smtp">>;
|
|
70
116
|
export declare const CalendarFreeBusyOperations: Record<CalendarFreeBusyOperationIds, OperationHandler<CalendarFreeBusyOperationIds>>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { applyScopedDelete, applyScopedUpdate, buildEventCalendar, deleteCalendarObject, followingUidOf, listBusySpans, listCalendarInstances, listObjectInstances, OFFSET_BEARING, parseCalendar, prepareCalendarObject, putCalendarObject, readWrittenObject, toOffsetIso, toUtcIso, } from "@remit/calendar-service";
|
|
1
|
+
import { applyGuestList, applyScopedDelete, applyScopedUpdate, attendeesOf, buildEventCalendar, buildSchedulingMessage, countAttendees, deleteCalendarObject, followingUidOf, listBusySpans, listCalendarInstances, listObjectInstances, OFFSET_BEARING, organizerOf, parseCalendar, participationOf, planCreateSends, planDeleteSends, planEventCreate, planEventUpdate, planUpdateSends, prepareCalendarObject, putCalendarObject, readWrittenObject, SUGGESTION_STATE_OF_ANSWER, schedulingBody, schedulingSubject, selfAddressSet, shapeScheduledUpdate, toOffsetIso, toUtcIso, } from "@remit/calendar-service";
|
|
2
2
|
import { base36uuid } from "@remit/data-ports/id";
|
|
3
|
-
import { RecurrenceScope } from "@remit/domain-enums";
|
|
3
|
+
import { CalendarComponentSet, RecurrenceScope } from "@remit/domain-enums";
|
|
4
4
|
import { badRequest, calendarDepsOf, findCalendarFor, findWritableCalendarFor, listCalendarsFor, notFound, preconditionFailed, refuseCalendar, } from "./calendar.js";
|
|
5
|
+
import { chosenInvitationSender } from "./user-settings.js";
|
|
5
6
|
/**
|
|
6
7
|
* The widest window a single read may ask for. A year covers every view the
|
|
7
8
|
* client has — a year grid is the coarsest — and keeps one request from asking
|
|
@@ -9,10 +10,11 @@ import { badRequest, calendarDepsOf, findCalendarFor, findWritableCalendarFor, l
|
|
|
9
10
|
*/
|
|
10
11
|
export const CALENDAR_MAX_WINDOW_DAYS = 366;
|
|
11
12
|
const MAX_WINDOW_MS = CALENDAR_MAX_WINDOW_DAYS * 24 * 60 * 60 * 1000;
|
|
12
|
-
export const calendarEventDepsOf = (deps) => ({
|
|
13
|
+
export const calendarEventDepsOf = (deps, queueScheduled) => ({
|
|
13
14
|
...deps,
|
|
14
15
|
newId: base36uuid,
|
|
15
16
|
now: () => new Date(),
|
|
17
|
+
queueScheduled,
|
|
16
18
|
});
|
|
17
19
|
/**
|
|
18
20
|
* Reads the window a listing asked for.
|
|
@@ -41,11 +43,18 @@ export const readWindow = (from, to) => {
|
|
|
41
43
|
}
|
|
42
44
|
return { ok: true, value: { from: toUtcIso(fromMs), to: toUtcIso(toMs) } };
|
|
43
45
|
};
|
|
44
|
-
/**
|
|
45
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The collections a listing covers, defaulting to every one the caller holds
|
|
48
|
+
* of the component set asked for.
|
|
49
|
+
*/
|
|
50
|
+
export const collectionsForListing = async (deps, accountConfigId, calendarIds, componentSet = CalendarComponentSet.VeventOnly) => {
|
|
46
51
|
const held = await listCalendarsFor(deps, accountConfigId);
|
|
47
|
-
if (calendarIds.length === 0)
|
|
48
|
-
return {
|
|
52
|
+
if (calendarIds.length === 0) {
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
value: held.filter((collection) => collection.componentSet === componentSet),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
49
58
|
const selected = held.filter((collection) => calendarIds.includes(collection.calendarId));
|
|
50
59
|
const missing = calendarIds.find((calendarId) => !held.some((collection) => collection.calendarId === calendarId));
|
|
51
60
|
if (missing) {
|
|
@@ -53,6 +62,9 @@ export const collectionsForListing = async (deps, accountConfigId, calendarIds)
|
|
|
53
62
|
}
|
|
54
63
|
return { ok: true, value: selected };
|
|
55
64
|
};
|
|
65
|
+
/** The account config's accounts: who the reader is, and who can send. */
|
|
66
|
+
const accountsOf = (deps, accountConfigId) => deps.accounts.listAllByAccountConfig(accountConfigId);
|
|
67
|
+
const selfOf = (accounts) => selfAddressSet(accounts.map((account) => account.email));
|
|
56
68
|
const toInstanceResponse = (instance) => ({
|
|
57
69
|
calendarId: instance.calendarId,
|
|
58
70
|
calendarObjectId: instance.calendarObjectId,
|
|
@@ -67,28 +79,70 @@ const toInstanceResponse = (instance) => ({
|
|
|
67
79
|
zoneCertainty: instance.zoneCertainty,
|
|
68
80
|
etag: instance.etag,
|
|
69
81
|
hasRecurrence: instance.hasRecurrence,
|
|
82
|
+
participation: instance.participation,
|
|
70
83
|
});
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
const parsedOrThrow = async (object) => {
|
|
85
|
+
const parsed = await parseCalendar(object.icalData);
|
|
86
|
+
if (!parsed.ok) {
|
|
87
|
+
throw new Error(`stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`);
|
|
88
|
+
}
|
|
89
|
+
return parsed.value;
|
|
90
|
+
};
|
|
91
|
+
/** The scheduling messages about an event still in the outbox. */
|
|
92
|
+
const pendingSends = async (deps, accountConfigId, calendarObjectId) => {
|
|
93
|
+
const parts = await deps.outboxSchedulingPart.listByCalendarObject(accountConfigId, calendarObjectId);
|
|
94
|
+
if (parts.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const rows = await deps.outboxMessage.get(accountConfigId, parts.map((part) => part.outboxMessageId));
|
|
97
|
+
const byId = new Map(rows.map((row) => [row.outboxMessageId, row]));
|
|
98
|
+
return parts.flatMap((part) => {
|
|
99
|
+
const row = byId.get(part.outboxMessageId);
|
|
100
|
+
return row
|
|
101
|
+
? [
|
|
102
|
+
{
|
|
103
|
+
email: part.recipient,
|
|
104
|
+
outboxMessageId: row.outboxMessageId,
|
|
105
|
+
method: part.method,
|
|
106
|
+
status: row.status,
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
: [];
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* An event as the API returns it: the stored row, plus the guest list read
|
|
114
|
+
* from its bytes and the reader's own place on it.
|
|
115
|
+
*/
|
|
116
|
+
export const toEventResponse = async (object, self, sends) => {
|
|
117
|
+
const { master } = await parsedOrThrow(object);
|
|
118
|
+
const attendees = attendeesOf(master);
|
|
119
|
+
return {
|
|
120
|
+
calendarObjectId: object.calendarObjectId,
|
|
121
|
+
calendarId: object.calendarId,
|
|
122
|
+
resourceName: object.resourceName,
|
|
123
|
+
icalUid: object.icalUid,
|
|
124
|
+
icalData: object.icalData,
|
|
125
|
+
etag: object.etag,
|
|
126
|
+
sequence: object.sequence,
|
|
127
|
+
syncSequence: object.syncSequence,
|
|
128
|
+
summary: object.summary,
|
|
129
|
+
dtStart: object.dtStart,
|
|
130
|
+
dtEnd: object.dtEnd,
|
|
131
|
+
allDay: object.allDay,
|
|
132
|
+
zoneCertainty: object.zoneCertainty,
|
|
133
|
+
status: object.status,
|
|
134
|
+
transparency: object.transparency,
|
|
135
|
+
hasRecurrence: object.hasRecurrence,
|
|
136
|
+
expandedThrough: object.expandedThrough,
|
|
137
|
+
createdAt: object.createdAt,
|
|
138
|
+
updatedAt: object.updatedAt,
|
|
139
|
+
participation: participationOf(master, self),
|
|
140
|
+
organizer: organizerOf(master),
|
|
141
|
+
attendees,
|
|
142
|
+
attendeeCounts: countAttendees(attendees),
|
|
143
|
+
sends,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
92
146
|
const eventFieldsOf = (input) => ({
|
|
93
147
|
summary: input.summary,
|
|
94
148
|
description: input.description ?? "",
|
|
@@ -127,6 +181,13 @@ export const pickEventUpdate = (body) => {
|
|
|
127
181
|
}
|
|
128
182
|
return patch;
|
|
129
183
|
};
|
|
184
|
+
export const pickSchedulingUpdate = (body) => ({
|
|
185
|
+
...(body.attendees !== undefined && { attendees: body.attendees }),
|
|
186
|
+
...(body.organizerAccountId !== undefined && {
|
|
187
|
+
organizerAccountId: body.organizerAccountId,
|
|
188
|
+
}),
|
|
189
|
+
...(body.answer !== undefined && { answer: body.answer }),
|
|
190
|
+
});
|
|
130
191
|
/**
|
|
131
192
|
* Whether an `If-Match` header lets the write through.
|
|
132
193
|
*
|
|
@@ -161,23 +222,14 @@ export const readScope = (value) => {
|
|
|
161
222
|
return refuseCalendar("InvalidScope", `scope must be one of This, Following or All, and this request sent "${String(value)}"`);
|
|
162
223
|
};
|
|
163
224
|
/**
|
|
164
|
-
*
|
|
225
|
+
* Turns what a scoped edit resolved to into the store writes it is.
|
|
165
226
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
* user sees twice or not at all.
|
|
227
|
+
* Both halves of a split are prepared before either is written, so a refusal
|
|
228
|
+
* of either one commits nothing: half a split is never stored.
|
|
169
229
|
*/
|
|
170
|
-
|
|
171
|
-
if (write.kind === "Delete")
|
|
172
|
-
|
|
173
|
-
accountConfigId,
|
|
174
|
-
calendarId: object.calendarId,
|
|
175
|
-
calendarObjectId: object.calendarObjectId,
|
|
176
|
-
});
|
|
177
|
-
return { ok: true, value: null };
|
|
178
|
-
}
|
|
179
|
-
// Both resources are prepared before either is written, so a refusal of
|
|
180
|
-
// either one commits nothing: half a split is never stored.
|
|
230
|
+
const prepareScopedWrite = async (deps, accountConfigId, object, collectionTimezone, write) => {
|
|
231
|
+
if (write.kind === "Delete")
|
|
232
|
+
return { ok: true, value: { kind: "Delete", puts: [] } };
|
|
181
233
|
const zone = { timezone: collectionTimezone };
|
|
182
234
|
const head = await prepareCalendarObject(zone, {
|
|
183
235
|
accountConfigId,
|
|
@@ -188,11 +240,7 @@ export const commitScopedWrite = async (deps, accountConfigId, object, collectio
|
|
|
188
240
|
if (!head.ok)
|
|
189
241
|
return head;
|
|
190
242
|
if (write.kind === "Replace") {
|
|
191
|
-
|
|
192
|
-
return {
|
|
193
|
-
ok: true,
|
|
194
|
-
value: await readWrittenObject(deps.calendarStore, head.value),
|
|
195
|
-
};
|
|
243
|
+
return { ok: true, value: { kind: "Replace", puts: [head.value] } };
|
|
196
244
|
}
|
|
197
245
|
const following = await prepareCalendarObject(zone, {
|
|
198
246
|
accountConfigId,
|
|
@@ -202,29 +250,112 @@ export const commitScopedWrite = async (deps, accountConfigId, object, collectio
|
|
|
202
250
|
});
|
|
203
251
|
if (!following.ok)
|
|
204
252
|
return following;
|
|
205
|
-
await deps.calendarStore.commit([head.value, following.value]);
|
|
206
|
-
// The remainder is what the patch applied to, and it lives under an id the
|
|
207
|
-
// caller has never seen — returning the truncated head instead would leave
|
|
208
|
-
// them holding the resource their edit is not in.
|
|
209
253
|
return {
|
|
210
254
|
ok: true,
|
|
211
|
-
value:
|
|
255
|
+
value: { kind: "Split", puts: [head.value, following.value] },
|
|
212
256
|
};
|
|
213
257
|
};
|
|
258
|
+
/**
|
|
259
|
+
* Commits a prepared write and reads back what it stored, in write order. The
|
|
260
|
+
* last one is what the patch applied to: for a split, the remainder, under an
|
|
261
|
+
* id the caller has never seen.
|
|
262
|
+
*/
|
|
263
|
+
const commitPrepared = async (deps, accountConfigId, object, prepared) => {
|
|
264
|
+
if (prepared.kind === "Delete") {
|
|
265
|
+
await deleteCalendarObject(deps.calendarStore, {
|
|
266
|
+
accountConfigId,
|
|
267
|
+
calendarId: object.calendarId,
|
|
268
|
+
calendarObjectId: object.calendarObjectId,
|
|
269
|
+
});
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
await deps.calendarStore.commit(prepared.puts);
|
|
273
|
+
const written = [];
|
|
274
|
+
for (const put of prepared.puts) {
|
|
275
|
+
written.push(await readWrittenObject(deps.calendarStore, put));
|
|
276
|
+
}
|
|
277
|
+
return written;
|
|
278
|
+
};
|
|
279
|
+
/**
|
|
280
|
+
* Writes what a scoped edit resolved to.
|
|
281
|
+
*
|
|
282
|
+
* A split is two resources and one write set: the truncated original and the
|
|
283
|
+
* remainder land together or not at all, because half a split is a series the
|
|
284
|
+
* user sees twice or not at all.
|
|
285
|
+
*/
|
|
286
|
+
export const commitScopedWrite = async (deps, accountConfigId, object, collectionTimezone, write) => {
|
|
287
|
+
const prepared = await prepareScopedWrite(deps, accountConfigId, object, collectionTimezone, write);
|
|
288
|
+
if (!prepared.ok)
|
|
289
|
+
return prepared;
|
|
290
|
+
const written = await commitPrepared(deps, accountConfigId, object, prepared.value);
|
|
291
|
+
return { ok: true, value: written.at(-1) ?? null };
|
|
292
|
+
};
|
|
293
|
+
/**
|
|
294
|
+
* Queues every planned message, one outbox row each, from the account the plan
|
|
295
|
+
* names. The rows are the pending records; the send worker settles each one.
|
|
296
|
+
*/
|
|
297
|
+
const queueSends = async (deps, accountConfigId, sends) => {
|
|
298
|
+
if (!sends)
|
|
299
|
+
return [];
|
|
300
|
+
const queued = [];
|
|
301
|
+
for (const plan of sends.plans) {
|
|
302
|
+
const message = await buildSchedulingMessage(plan.icalData, {
|
|
303
|
+
method: plan.method,
|
|
304
|
+
now: deps.now(),
|
|
305
|
+
attendee: plan.attendee,
|
|
306
|
+
recurrenceId: plan.recurrenceId,
|
|
307
|
+
collectionTimezone: plan.collectionTimezone,
|
|
308
|
+
});
|
|
309
|
+
const subject = schedulingSubject(plan.method, message.summary, plan.answer);
|
|
310
|
+
const row = await deps.queueScheduled({
|
|
311
|
+
accountId: sends.from.accountId,
|
|
312
|
+
accountConfigId,
|
|
313
|
+
fromAddress: sends.from.email,
|
|
314
|
+
toAddresses: [plan.recipient.email],
|
|
315
|
+
subject,
|
|
316
|
+
textBody: schedulingBody(subject, message.icalData),
|
|
317
|
+
}, {
|
|
318
|
+
calendarObjectId: plan.calendarObjectId,
|
|
319
|
+
recipient: plan.recipient.email,
|
|
320
|
+
method: plan.method,
|
|
321
|
+
sequence: message.sequence,
|
|
322
|
+
icalData: message.icalData,
|
|
323
|
+
});
|
|
324
|
+
queued.push({
|
|
325
|
+
email: plan.recipient.email,
|
|
326
|
+
outboxMessageId: row.outboxMessageId,
|
|
327
|
+
method: plan.method,
|
|
328
|
+
status: row.status,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return queued;
|
|
332
|
+
};
|
|
214
333
|
export const createCalendarEventFor = async (deps, accountConfigId, input) => {
|
|
215
334
|
const collection = await findWritableCalendarFor(deps, accountConfigId, input.calendarId);
|
|
216
335
|
if (!collection.ok)
|
|
217
336
|
return collection;
|
|
337
|
+
const planned = planEventCreate(await accountsOf(deps, accountConfigId), await chosenInvitationSender(deps.accountSetting, accountConfigId), input.attendees ?? [], input.organizerAccountId);
|
|
338
|
+
if (!planned.ok)
|
|
339
|
+
return planned;
|
|
218
340
|
const id = deps.newId();
|
|
219
341
|
const built = await buildEventCalendar(eventFieldsOf(input), `${id}@reader.remit`, deps.now());
|
|
220
342
|
if (!built.ok)
|
|
221
343
|
return built;
|
|
222
|
-
|
|
344
|
+
const icalData = planned.value
|
|
345
|
+
? await applyGuestList(built.value, { email: planned.value.organizer.email, displayName: "" }, planned.value.guests)
|
|
346
|
+
: built.value;
|
|
347
|
+
const stored = await putCalendarObject(deps.calendarStore, {
|
|
223
348
|
accountConfigId,
|
|
224
349
|
calendarId: collection.value.calendarId,
|
|
225
350
|
resourceName: `${id}.ics`,
|
|
226
|
-
icalData
|
|
351
|
+
icalData,
|
|
227
352
|
});
|
|
353
|
+
if (!stored.ok)
|
|
354
|
+
return stored;
|
|
355
|
+
const sends = planned.value
|
|
356
|
+
? await queueSends(deps, accountConfigId, planCreateSends(planned.value, stored.value, collection.value.timezone))
|
|
357
|
+
: [];
|
|
358
|
+
return { ok: true, value: { object: stored.value, sends } };
|
|
228
359
|
};
|
|
229
360
|
const resolveResource = async (deps, accountConfigId, request, intent) => {
|
|
230
361
|
const collection = intent === "write"
|
|
@@ -238,47 +369,102 @@ const resolveResource = async (deps, accountConfigId, request, intent) => {
|
|
|
238
369
|
}
|
|
239
370
|
return { ok: true, value: { collection: collection.value, object } };
|
|
240
371
|
};
|
|
241
|
-
|
|
372
|
+
/**
|
|
373
|
+
* A card whose answer wrote this event follows a later answer given on the
|
|
374
|
+
* event, so the mail and the calendar never disagree about what was said.
|
|
375
|
+
*/
|
|
376
|
+
const settleAnsweredSuggestions = async (deps, accountConfigId, calendarObjectId, answer) => {
|
|
377
|
+
const cards = await deps.calendarStore.repos.calendarSuggestion.listByAcceptedCalendarObjects(accountConfigId, [calendarObjectId]);
|
|
378
|
+
if (cards.length === 0)
|
|
379
|
+
return;
|
|
380
|
+
await deps.calendarStore.commit(cards.map((card) => ({
|
|
381
|
+
kind: "SettleSuggestion",
|
|
382
|
+
accountConfigId,
|
|
383
|
+
suggestionId: card.suggestionId,
|
|
384
|
+
settle: {
|
|
385
|
+
state: SUGGESTION_STATE_OF_ANSWER[answer],
|
|
386
|
+
acceptedCalendarObjectId: calendarObjectId,
|
|
387
|
+
},
|
|
388
|
+
})));
|
|
389
|
+
};
|
|
390
|
+
const refuseStale = () => refuseCalendar("EtagMismatch", "this event has been written since you read it — read it again and reapply the change");
|
|
391
|
+
/**
|
|
392
|
+
* Edits an event, and queues what the edit owes its guests.
|
|
393
|
+
*
|
|
394
|
+
* The calendar row is local truth and is written first. The outbox rows are
|
|
395
|
+
* the pending records of the messages that follow it: an invitation to every
|
|
396
|
+
* guest of an event the reader organizes, a cancellation to a guest the edit
|
|
397
|
+
* removed, a reply to the organizer when the reader answers as a guest. The
|
|
398
|
+
* outbox lives in another database and is not written in the calendar's
|
|
399
|
+
* commit; a later edit made while an earlier message is still queued does not
|
|
400
|
+
* wait for it, and each message carries the SEQUENCE of the bytes it was built
|
|
401
|
+
* from, so a recipient keeps the newest.
|
|
402
|
+
*/
|
|
403
|
+
export const updateCalendarEventFor = async (deps, accountConfigId, request, patch, scheduling = {}) => {
|
|
242
404
|
const resolved = await resolveResource(deps, accountConfigId, request, "write");
|
|
243
405
|
if (!resolved.ok)
|
|
244
406
|
return resolved;
|
|
245
407
|
const { collection, object } = resolved.value;
|
|
246
|
-
if (!etagMatches(request.ifMatch, object.etag))
|
|
247
|
-
return
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const write = await applyScopedUpdate(parsed
|
|
408
|
+
if (!etagMatches(request.ifMatch, object.etag))
|
|
409
|
+
return refuseStale();
|
|
410
|
+
const parsed = await parsedOrThrow(object);
|
|
411
|
+
const accounts = await accountsOf(deps, accountConfigId);
|
|
412
|
+
const planned = planEventUpdate(parsed, accounts, await chosenInvitationSender(deps.accountSetting, accountConfigId), request.scope, scheduling);
|
|
413
|
+
if (!planned.ok)
|
|
414
|
+
return planned;
|
|
415
|
+
const write = await applyScopedUpdate(parsed, collection.timezone, {
|
|
254
416
|
scope: request.scope,
|
|
255
417
|
recurrenceId: request.recurrenceId,
|
|
256
418
|
followingUid: followingUidOf(object.icalUid, `${deps.newId()}@reader.remit`),
|
|
257
419
|
}, patch);
|
|
258
420
|
if (!write.ok)
|
|
259
421
|
return write;
|
|
260
|
-
|
|
422
|
+
if (write.value.kind === "Delete") {
|
|
423
|
+
throw new Error("a scoped update resolved to a delete");
|
|
424
|
+
}
|
|
425
|
+
const shaped = await shapeScheduledUpdate(write.value, planned.value, scheduling, patch, collection.timezone, request.scope, request.recurrenceId);
|
|
426
|
+
if (!shaped.ok)
|
|
427
|
+
return shaped;
|
|
428
|
+
const prepared = await prepareScopedWrite(deps, accountConfigId, object, collection.timezone, shaped.value);
|
|
429
|
+
if (!prepared.ok)
|
|
430
|
+
return prepared;
|
|
431
|
+
const written = await commitPrepared(deps, accountConfigId, object, prepared.value);
|
|
432
|
+
const result = written.at(-1) ?? null;
|
|
433
|
+
if (scheduling.answer !== undefined &&
|
|
434
|
+
request.scope === RecurrenceScope.All &&
|
|
435
|
+
result) {
|
|
436
|
+
await settleAnsweredSuggestions(deps, accountConfigId, result.calendarObjectId, scheduling.answer);
|
|
437
|
+
}
|
|
438
|
+
const sends = await queueSends(deps, accountConfigId, await planUpdateSends(planned.value, accounts, scheduling, object, written, collection.timezone, request.scope, request.recurrenceId));
|
|
439
|
+
return { ok: true, value: { object: result, sends } };
|
|
261
440
|
};
|
|
441
|
+
/**
|
|
442
|
+
* Deletes an event, or one occurrence of it, and queues what that owes its
|
|
443
|
+
* guests: a cancellation each when the whole event goes, the shortened event
|
|
444
|
+
* when part of a series does. Only an event the reader organizes owes anything.
|
|
445
|
+
*/
|
|
262
446
|
export const deleteCalendarEventFor = async (deps, accountConfigId, request) => {
|
|
263
447
|
const resolved = await resolveResource(deps, accountConfigId, request, "write");
|
|
264
448
|
if (!resolved.ok)
|
|
265
449
|
return resolved;
|
|
266
450
|
const { collection, object } = resolved.value;
|
|
267
|
-
if (!etagMatches(request.ifMatch, object.etag))
|
|
268
|
-
return
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
if (!parsed.ok) {
|
|
272
|
-
throw new Error(`stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`);
|
|
273
|
-
}
|
|
274
|
-
const write = await applyScopedDelete(parsed.value, collection.timezone, {
|
|
451
|
+
if (!etagMatches(request.ifMatch, object.etag))
|
|
452
|
+
return refuseStale();
|
|
453
|
+
const parsed = await parsedOrThrow(object);
|
|
454
|
+
const write = await applyScopedDelete(parsed, collection.timezone, {
|
|
275
455
|
scope: request.scope,
|
|
276
456
|
recurrenceId: request.recurrenceId,
|
|
277
457
|
followingUid: "",
|
|
278
458
|
});
|
|
279
459
|
if (!write.ok)
|
|
280
460
|
return write;
|
|
281
|
-
|
|
461
|
+
const prepared = await prepareScopedWrite(deps, accountConfigId, object, collection.timezone, write.value);
|
|
462
|
+
if (!prepared.ok)
|
|
463
|
+
return prepared;
|
|
464
|
+
const written = await commitPrepared(deps, accountConfigId, object, prepared.value);
|
|
465
|
+
const result = written.at(-1) ?? null;
|
|
466
|
+
const sends = await queueSends(deps, accountConfigId, planDeleteSends(parsed, await accountsOf(deps, accountConfigId), object, result, collection.timezone));
|
|
467
|
+
return { ok: true, value: { object: result, sends } };
|
|
282
468
|
};
|
|
283
469
|
/** The collection holding a resource, found among the ones given. */
|
|
284
470
|
const locateResource = async (deps, collections, calendarObjectId) => {
|
|
@@ -296,19 +482,21 @@ const readCalendarIds = (value) => {
|
|
|
296
482
|
return value.filter((id) => typeof id === "string");
|
|
297
483
|
return [];
|
|
298
484
|
};
|
|
299
|
-
const answerRefusal = (error) => error.code === "NotFound"
|
|
485
|
+
export const answerRefusal = (error) => error.code === "NotFound"
|
|
300
486
|
? notFound(error.message)
|
|
301
487
|
: error.code === "EtagMismatch"
|
|
302
488
|
? preconditionFailed(error.message)
|
|
303
489
|
: badRequest(error);
|
|
490
|
+
export const readIfMatch = (context) => {
|
|
491
|
+
const headers = (context.request.headers ?? {});
|
|
492
|
+
return Object.entries(headers).find(([name]) => name.toLowerCase() === "if-match")?.[1];
|
|
493
|
+
};
|
|
304
494
|
const scopedRequestOf = (context) => {
|
|
305
495
|
const params = context.request.params;
|
|
306
496
|
const query = context.request.query;
|
|
307
497
|
const scope = readScope(query.scope);
|
|
308
498
|
if (!scope.ok)
|
|
309
499
|
return scope;
|
|
310
|
-
const headers = (context.request.headers ?? {});
|
|
311
|
-
const ifMatch = Object.entries(headers).find(([name]) => name.toLowerCase() === "if-match")?.[1];
|
|
312
500
|
return {
|
|
313
501
|
ok: true,
|
|
314
502
|
value: {
|
|
@@ -316,10 +504,11 @@ const scopedRequestOf = (context) => {
|
|
|
316
504
|
calendarObjectId: params.calendarObjectId,
|
|
317
505
|
scope: scope.value,
|
|
318
506
|
recurrenceId: query.recurrenceId ?? "",
|
|
319
|
-
ifMatch,
|
|
507
|
+
ifMatch: readIfMatch(context),
|
|
320
508
|
},
|
|
321
509
|
};
|
|
322
510
|
};
|
|
511
|
+
const writeDepsOf = (ctx) => calendarEventDepsOf(calendarDepsOf(ctx.client), (input, part) => ctx.client.outboxQueue.createAndSendScheduled(input, part));
|
|
323
512
|
export const CalendarEventOperations = {
|
|
324
513
|
CalendarEventOperations_listCalendarEvents: async (context, ctx) => {
|
|
325
514
|
const accountConfigId = ctx.identity.accountConfigId;
|
|
@@ -331,24 +520,25 @@ export const CalendarEventOperations = {
|
|
|
331
520
|
const collections = await collectionsForListing(deps, accountConfigId, readCalendarIds(query.calendarId));
|
|
332
521
|
if (!collections.ok)
|
|
333
522
|
return answerRefusal(collections.error);
|
|
523
|
+
const self = selfOf(await accountsOf(deps, accountConfigId));
|
|
334
524
|
if (query.calendarObjectId) {
|
|
335
525
|
const located = await locateResource(deps, collections.value, query.calendarObjectId);
|
|
336
526
|
if (!located)
|
|
337
527
|
return { items: [] };
|
|
338
|
-
const own = await listObjectInstances(deps, located.collection, located.object, window.value);
|
|
528
|
+
const own = await listObjectInstances(deps, located.collection, located.object, window.value, self);
|
|
339
529
|
return { items: own.map(toInstanceResponse) };
|
|
340
530
|
}
|
|
341
|
-
const instances = await listCalendarInstances(deps, collections.value, window.value);
|
|
531
|
+
const instances = await listCalendarInstances(deps, collections.value, window.value, self);
|
|
342
532
|
return { items: instances.map(toInstanceResponse) };
|
|
343
533
|
},
|
|
344
534
|
CalendarEventOperations_createCalendarEvent: async (context, ctx) => {
|
|
345
535
|
const accountConfigId = ctx.identity.accountConfigId;
|
|
346
536
|
const input = context.request.requestBody;
|
|
347
|
-
const deps =
|
|
537
|
+
const deps = writeDepsOf(ctx);
|
|
348
538
|
const created = await createCalendarEventFor(deps, accountConfigId, input);
|
|
349
539
|
if (!created.ok)
|
|
350
540
|
return answerRefusal(created.error);
|
|
351
|
-
return toEventResponse(created.value);
|
|
541
|
+
return toEventResponse(created.value.object, selfOf(await accountsOf(deps, accountConfigId)), created.value.sends);
|
|
352
542
|
},
|
|
353
543
|
};
|
|
354
544
|
export const CalendarEventDetailOperations = {
|
|
@@ -357,18 +547,20 @@ export const CalendarEventDetailOperations = {
|
|
|
357
547
|
const request = scopedRequestOf(context);
|
|
358
548
|
if (!request.ok)
|
|
359
549
|
return badRequest(request.error);
|
|
360
|
-
const deps =
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
return
|
|
550
|
+
const deps = calendarDepsOf(ctx.client);
|
|
551
|
+
const self = selfOf(await accountsOf(deps, accountConfigId));
|
|
552
|
+
const found = request.value.calendarId === ""
|
|
553
|
+
? await locateResource(deps, await listCalendarsFor(deps, accountConfigId), request.value.calendarObjectId)
|
|
554
|
+
: undefined;
|
|
555
|
+
if (request.value.calendarId === "" && !found) {
|
|
556
|
+
return notFound(`no event ${request.value.calendarObjectId} on this account`);
|
|
367
557
|
}
|
|
368
|
-
const resolved =
|
|
558
|
+
const resolved = found
|
|
559
|
+
? { ok: true, value: found }
|
|
560
|
+
: await resolveResource(deps, accountConfigId, request.value, "read");
|
|
369
561
|
if (!resolved.ok)
|
|
370
562
|
return answerRefusal(resolved.error);
|
|
371
|
-
return toEventResponse(resolved.value.object);
|
|
563
|
+
return toEventResponse(resolved.value.object, self, await pendingSends(deps, accountConfigId, resolved.value.object.calendarObjectId));
|
|
372
564
|
},
|
|
373
565
|
CalendarEventDetailOperations_updateCalendarEvent: async (context, ctx) => {
|
|
374
566
|
const accountConfigId = ctx.identity.accountConfigId;
|
|
@@ -377,25 +569,24 @@ export const CalendarEventDetailOperations = {
|
|
|
377
569
|
const request = scopedRequestOf(context);
|
|
378
570
|
if (!request.ok)
|
|
379
571
|
return badRequest(request.error);
|
|
380
|
-
const deps =
|
|
381
|
-
const updated = await updateCalendarEventFor(deps, accountConfigId, request.value, pickEventUpdate(body));
|
|
572
|
+
const deps = writeDepsOf(ctx);
|
|
573
|
+
const updated = await updateCalendarEventFor(deps, accountConfigId, request.value, pickEventUpdate(body), pickSchedulingUpdate(body));
|
|
382
574
|
if (!updated.ok)
|
|
383
575
|
return answerRefusal(updated.error);
|
|
384
|
-
if (!updated.value) {
|
|
576
|
+
if (!updated.value.object) {
|
|
385
577
|
throw new Error("a scoped update resolved to a delete");
|
|
386
578
|
}
|
|
387
|
-
return toEventResponse(updated.value);
|
|
579
|
+
return toEventResponse(updated.value.object, selfOf(await accountsOf(deps, accountConfigId)), updated.value.sends);
|
|
388
580
|
},
|
|
389
581
|
CalendarEventDetailOperations_deleteCalendarEvent: async (context, ctx) => {
|
|
390
582
|
const accountConfigId = ctx.identity.accountConfigId;
|
|
391
583
|
const request = scopedRequestOf(context);
|
|
392
584
|
if (!request.ok)
|
|
393
585
|
return badRequest(request.error);
|
|
394
|
-
const
|
|
395
|
-
const removed = await deleteCalendarEventFor(deps, accountConfigId, request.value);
|
|
586
|
+
const removed = await deleteCalendarEventFor(writeDepsOf(ctx), accountConfigId, request.value);
|
|
396
587
|
if (!removed.ok)
|
|
397
588
|
return answerRefusal(removed.error);
|
|
398
|
-
return { statusCode:
|
|
589
|
+
return { statusCode: 200, body: { sends: removed.value.sends } };
|
|
399
590
|
},
|
|
400
591
|
};
|
|
401
592
|
export const CalendarFreeBusyOperations = {
|
|
@@ -406,8 +597,8 @@ export const CalendarFreeBusyOperations = {
|
|
|
406
597
|
if (!window.ok)
|
|
407
598
|
return badRequest(window.error);
|
|
408
599
|
const deps = calendarDepsOf(ctx.client);
|
|
409
|
-
const collections = await listCalendarsFor(deps, accountConfigId);
|
|
410
|
-
const spans = await listBusySpans(deps, collections, window.value);
|
|
600
|
+
const collections = (await listCalendarsFor(deps, accountConfigId)).filter((collection) => collection.componentSet === CalendarComponentSet.VeventOnly);
|
|
601
|
+
const spans = await listBusySpans(deps, collections, window.value, selfOf(await accountsOf(deps, accountConfigId)));
|
|
411
602
|
const items = spans.map((span) => ({
|
|
412
603
|
start: toOffsetIso(span.startMs, "UTC"),
|
|
413
604
|
end: toOffsetIso(span.endMs, "UTC"),
|
|
@@ -136,6 +136,9 @@ export const CalendarSuggestionActionOperations = {
|
|
|
136
136
|
.request.requestBody;
|
|
137
137
|
const client = ctx.client;
|
|
138
138
|
const suggestion = await client.calendarSuggestion.get(accountConfigId, suggestionId);
|
|
139
|
+
if (answer === CalendarInviteAnswer.Declined) {
|
|
140
|
+
throw new BadRequestError("Accepting cannot decline an invitation. Decline it with decline instead.");
|
|
141
|
+
}
|
|
139
142
|
if (answer === CalendarInviteAnswer.Tentative &&
|
|
140
143
|
suggestion.method === CalendarInviteMethod.Cancel) {
|
|
141
144
|
throw new BadRequestError("This is a cancellation, so there is nothing to answer maybe to.");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CreateCalendarTaskInput } from "@remit/api-openapi-types";
|
|
2
|
+
import { type CalendarTaskFields } from "@remit/calendar-service";
|
|
3
|
+
import type { CalendarCollectionItem, CalendarTaskItem } from "@remit/data-ports";
|
|
4
|
+
import type { CalendarTaskDetailOperationIds, CalendarTaskOperationIds, OperationHandler } from "../types.js";
|
|
5
|
+
import { type CalendarDeps, type CalendarOutcome } from "./calendar.js";
|
|
6
|
+
/** The task collection a write goes to: the one named, or the account's own. */
|
|
7
|
+
export declare const taskCalendarFor: (deps: CalendarDeps, accountConfigId: string, calendarId: string) => Promise<CalendarOutcome<CalendarCollectionItem>>;
|
|
8
|
+
export declare const createCalendarTaskFor: (deps: CalendarDeps, accountConfigId: string, input: CreateCalendarTaskInput, newId?: () => string, now?: Date) => Promise<CalendarOutcome<CalendarTaskItem>>;
|
|
9
|
+
interface TaskRequest {
|
|
10
|
+
calendarId: string;
|
|
11
|
+
calendarTaskId: string;
|
|
12
|
+
ifMatch: string | undefined;
|
|
13
|
+
}
|
|
14
|
+
export declare const updateCalendarTaskFor: (deps: CalendarDeps, accountConfigId: string, request: TaskRequest, patch: Partial<CalendarTaskFields>, now?: Date) => Promise<CalendarOutcome<CalendarTaskItem>>;
|
|
15
|
+
export declare const CalendarTaskOperations: Record<CalendarTaskOperationIds, OperationHandler<CalendarTaskOperationIds>>;
|
|
16
|
+
export declare const CalendarTaskDetailOperations: Record<CalendarTaskDetailOperationIds, OperationHandler<CalendarTaskDetailOperationIds>>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { buildTaskCalendar, listTasksInWindow, prepareCalendarTask, updateTaskCalendar, } from "@remit/calendar-service";
|
|
2
|
+
import { base36uuid } from "@remit/data-ports/id";
|
|
3
|
+
import { CalendarComponentSet } from "@remit/domain-enums";
|
|
4
|
+
import { calendarDepsOf, listCalendarsFor, notFound, refuseCalendar, } from "./calendar.js";
|
|
5
|
+
import { answerRefusal, collectionsForListing, etagMatches, readIfMatch, readWindow, } from "./calendar-event.js";
|
|
6
|
+
const toTaskResponse = (task) => ({
|
|
7
|
+
calendarTaskId: task.calendarTaskId,
|
|
8
|
+
calendarId: task.calendarId,
|
|
9
|
+
resourceName: task.resourceName,
|
|
10
|
+
icalUid: task.icalUid,
|
|
11
|
+
icalData: task.icalData,
|
|
12
|
+
etag: task.etag,
|
|
13
|
+
summary: task.summary,
|
|
14
|
+
description: task.description,
|
|
15
|
+
due: task.due,
|
|
16
|
+
dueAt: task.dueAt,
|
|
17
|
+
allDay: task.allDay,
|
|
18
|
+
completed: task.completed,
|
|
19
|
+
createdAt: task.createdAt,
|
|
20
|
+
updatedAt: task.updatedAt,
|
|
21
|
+
});
|
|
22
|
+
const isTaskCollection = (collection) => collection.componentSet === CalendarComponentSet.VtodoOnly;
|
|
23
|
+
/** The task collection a write goes to: the one named, or the account's own. */
|
|
24
|
+
export const taskCalendarFor = async (deps, accountConfigId, calendarId) => {
|
|
25
|
+
const held = await listCalendarsFor(deps, accountConfigId);
|
|
26
|
+
if (calendarId === "") {
|
|
27
|
+
const own = held.find(isTaskCollection);
|
|
28
|
+
if (!own)
|
|
29
|
+
throw new Error("listCalendarsFor provisioned no task collection");
|
|
30
|
+
return { ok: true, value: own };
|
|
31
|
+
}
|
|
32
|
+
const named = held.find((collection) => collection.calendarId === calendarId);
|
|
33
|
+
if (!named) {
|
|
34
|
+
return refuseCalendar("NotFound", `no calendar ${calendarId} on this account`);
|
|
35
|
+
}
|
|
36
|
+
if (!isTaskCollection(named)) {
|
|
37
|
+
return refuseCalendar("NotATaskCalendar", `"${named.displayName}" holds events, so a task cannot be written into it`);
|
|
38
|
+
}
|
|
39
|
+
return { ok: true, value: named };
|
|
40
|
+
};
|
|
41
|
+
const pickTaskUpdate = (body) => {
|
|
42
|
+
const patch = {};
|
|
43
|
+
for (const field of [
|
|
44
|
+
"summary",
|
|
45
|
+
"description",
|
|
46
|
+
"due",
|
|
47
|
+
"allDay",
|
|
48
|
+
"timeZone",
|
|
49
|
+
"completed",
|
|
50
|
+
]) {
|
|
51
|
+
if (!Object.hasOwn(body, field))
|
|
52
|
+
continue;
|
|
53
|
+
Object.assign(patch, { [field]: body[field] });
|
|
54
|
+
}
|
|
55
|
+
return patch;
|
|
56
|
+
};
|
|
57
|
+
export const createCalendarTaskFor = async (deps, accountConfigId, input, newId = base36uuid, now = new Date()) => {
|
|
58
|
+
const collection = await taskCalendarFor(deps, accountConfigId, input.calendarId ?? "");
|
|
59
|
+
if (!collection.ok)
|
|
60
|
+
return collection;
|
|
61
|
+
const id = newId();
|
|
62
|
+
const built = buildTaskCalendar({
|
|
63
|
+
summary: input.summary,
|
|
64
|
+
description: input.description ?? "",
|
|
65
|
+
due: input.due,
|
|
66
|
+
allDay: input.allDay ?? false,
|
|
67
|
+
timeZone: input.timeZone ?? "",
|
|
68
|
+
completed: false,
|
|
69
|
+
}, `${id}@reader.remit`, now);
|
|
70
|
+
if (!built.ok)
|
|
71
|
+
return built;
|
|
72
|
+
const row = await prepareCalendarTask(collection.value, `${id}.ics`, built.value);
|
|
73
|
+
if (!row.ok)
|
|
74
|
+
return row;
|
|
75
|
+
return { ok: true, value: await deps.calendarTask.put(row.value) };
|
|
76
|
+
};
|
|
77
|
+
const resolveTask = async (deps, accountConfigId, request) => {
|
|
78
|
+
const collection = await taskCalendarFor(deps, accountConfigId, request.calendarId);
|
|
79
|
+
if (!collection.ok)
|
|
80
|
+
return collection;
|
|
81
|
+
const task = await deps.calendarTask.find(collection.value.calendarId, request.calendarTaskId);
|
|
82
|
+
if (!task) {
|
|
83
|
+
return refuseCalendar("NotFound", `no task ${request.calendarTaskId} in this calendar`);
|
|
84
|
+
}
|
|
85
|
+
if (!etagMatches(request.ifMatch, task.etag)) {
|
|
86
|
+
return refuseCalendar("EtagMismatch", "this task has been written since you read it — read it again and reapply the change");
|
|
87
|
+
}
|
|
88
|
+
return { ok: true, value: { collection: collection.value, task } };
|
|
89
|
+
};
|
|
90
|
+
export const updateCalendarTaskFor = async (deps, accountConfigId, request, patch, now = new Date()) => {
|
|
91
|
+
const resolved = await resolveTask(deps, accountConfigId, request);
|
|
92
|
+
if (!resolved.ok)
|
|
93
|
+
return resolved;
|
|
94
|
+
const { collection, task } = resolved.value;
|
|
95
|
+
const rewritten = await updateTaskCalendar(task.icalData, patch, collection.timezone, now);
|
|
96
|
+
if (!rewritten.ok)
|
|
97
|
+
return rewritten;
|
|
98
|
+
const row = await prepareCalendarTask(collection, task.resourceName, rewritten.value);
|
|
99
|
+
if (!row.ok)
|
|
100
|
+
return row;
|
|
101
|
+
return { ok: true, value: await deps.calendarTask.put(row.value) };
|
|
102
|
+
};
|
|
103
|
+
const taskRequestOf = (context) => {
|
|
104
|
+
const { calendarTaskId } = context.request.params;
|
|
105
|
+
const { calendarId } = context.request.query;
|
|
106
|
+
return {
|
|
107
|
+
calendarId: calendarId ?? "",
|
|
108
|
+
calendarTaskId,
|
|
109
|
+
ifMatch: readIfMatch(context),
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
const readCalendarIds = (value) => {
|
|
113
|
+
if (typeof value === "string")
|
|
114
|
+
return value === "" ? [] : [value];
|
|
115
|
+
if (Array.isArray(value))
|
|
116
|
+
return value.filter((id) => typeof id === "string");
|
|
117
|
+
return [];
|
|
118
|
+
};
|
|
119
|
+
export const CalendarTaskOperations = {
|
|
120
|
+
CalendarTaskOperations_listCalendarTasks: async (context, ctx) => {
|
|
121
|
+
const accountConfigId = ctx.identity.accountConfigId;
|
|
122
|
+
const query = context.request.query;
|
|
123
|
+
const window = readWindow(query.from, query.to);
|
|
124
|
+
if (!window.ok)
|
|
125
|
+
return answerRefusal(window.error);
|
|
126
|
+
const deps = calendarDepsOf(ctx.client);
|
|
127
|
+
const collections = await collectionsForListing(deps, accountConfigId, readCalendarIds(query.calendarId), CalendarComponentSet.VtodoOnly);
|
|
128
|
+
if (!collections.ok)
|
|
129
|
+
return answerRefusal(collections.error);
|
|
130
|
+
const tasks = await listTasksInWindow(deps.calendarTask, collections.value.filter(isTaskCollection), window.value);
|
|
131
|
+
return { items: tasks.map(toTaskResponse) };
|
|
132
|
+
},
|
|
133
|
+
CalendarTaskOperations_createCalendarTask: async (context, ctx) => {
|
|
134
|
+
const created = await createCalendarTaskFor(calendarDepsOf(ctx.client), ctx.identity.accountConfigId, context.request.requestBody);
|
|
135
|
+
if (!created.ok)
|
|
136
|
+
return answerRefusal(created.error);
|
|
137
|
+
return toTaskResponse(created.value);
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
export const CalendarTaskDetailOperations = {
|
|
141
|
+
CalendarTaskDetailOperations_getCalendarTask: async (context, ctx) => {
|
|
142
|
+
const accountConfigId = ctx.identity.accountConfigId;
|
|
143
|
+
const deps = calendarDepsOf(ctx.client);
|
|
144
|
+
const request = taskRequestOf(context);
|
|
145
|
+
if (request.calendarId === "") {
|
|
146
|
+
for (const collection of (await listCalendarsFor(deps, accountConfigId)).filter(isTaskCollection)) {
|
|
147
|
+
const task = await deps.calendarTask.find(collection.calendarId, request.calendarTaskId);
|
|
148
|
+
if (task)
|
|
149
|
+
return toTaskResponse(task);
|
|
150
|
+
}
|
|
151
|
+
return notFound(`no task ${request.calendarTaskId} on this account`);
|
|
152
|
+
}
|
|
153
|
+
const resolved = await resolveTask(deps, accountConfigId, {
|
|
154
|
+
...request,
|
|
155
|
+
ifMatch: undefined,
|
|
156
|
+
});
|
|
157
|
+
if (!resolved.ok)
|
|
158
|
+
return answerRefusal(resolved.error);
|
|
159
|
+
return toTaskResponse(resolved.value.task);
|
|
160
|
+
},
|
|
161
|
+
CalendarTaskDetailOperations_updateCalendarTask: async (context, ctx) => {
|
|
162
|
+
const updated = await updateCalendarTaskFor(calendarDepsOf(ctx.client), ctx.identity.accountConfigId, taskRequestOf(context), pickTaskUpdate(context.request.requestBody));
|
|
163
|
+
if (!updated.ok)
|
|
164
|
+
return answerRefusal(updated.error);
|
|
165
|
+
return toTaskResponse(updated.value);
|
|
166
|
+
},
|
|
167
|
+
CalendarTaskDetailOperations_deleteCalendarTask: async (context, ctx) => {
|
|
168
|
+
const deps = calendarDepsOf(ctx.client);
|
|
169
|
+
const resolved = await resolveTask(deps, ctx.identity.accountConfigId, taskRequestOf(context));
|
|
170
|
+
if (!resolved.ok)
|
|
171
|
+
return answerRefusal(resolved.error);
|
|
172
|
+
await deps.calendarTask.delete(resolved.value.collection.calendarId, resolved.value.task.calendarTaskId);
|
|
173
|
+
return { statusCode: 204 };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CreateCalendarInput, UpdateCalendarInput } from "@remit/api-openapi-types";
|
|
2
2
|
import { type CalendarFeedFetcher } from "@remit/calendar-service";
|
|
3
|
-
import type { CalendarCollectionItem, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, UpdateCalendarCollectionInput } from "@remit/data-ports";
|
|
3
|
+
import type { CalendarCollectionItem, IAccountRepository, IAccountSettingRepository, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, ICalendarTaskRepository, IOutboxMessageRepository, IOutboxSchedulingPartRepository, UpdateCalendarCollectionInput } from "@remit/data-ports";
|
|
4
4
|
import type { RemitClient } from "../service/data-client.js";
|
|
5
5
|
import type { CalendarCollectionDetailOperationIds, CalendarDetailOperationIds, CalendarOperationIds, OperationHandler } from "../types.js";
|
|
6
6
|
/** The calendar store, as the handlers reach it. */
|
|
@@ -10,6 +10,11 @@ export interface CalendarDeps {
|
|
|
10
10
|
calendarEventIndex: ICalendarEventIndexRepository;
|
|
11
11
|
calendarFeedToken: ICalendarFeedTokenRepository;
|
|
12
12
|
calendarStore: ICalendarStore;
|
|
13
|
+
calendarTask: ICalendarTaskRepository;
|
|
14
|
+
accounts: Pick<IAccountRepository, "listAllByAccountConfig">;
|
|
15
|
+
accountSetting: Pick<IAccountSettingRepository, "get">;
|
|
16
|
+
outboxMessage: Pick<IOutboxMessageRepository, "get">;
|
|
17
|
+
outboxSchedulingPart: Pick<IOutboxSchedulingPartRepository, "listByCalendarObject">;
|
|
13
18
|
}
|
|
14
19
|
/** Why a calendar request was refused, in a form a client can branch on. */
|
|
15
20
|
export interface CalendarRefusal {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { DEFAULT_CALENDAR_URL_SEGMENT, isResolvableZone, prepareCalendarObject, provisionDefaultCalendar, readCalendarFeed, readSubscriptionUrl, writeCalendarFeed, } from "@remit/calendar-service";
|
|
1
|
+
import { DEFAULT_CALENDAR_URL_SEGMENT, isResolvableZone, prepareCalendarObject, provisionDefaultCalendar, provisionTaskCalendar, readCalendarFeed, readSubscriptionUrl, writeCalendarFeed, } from "@remit/calendar-service";
|
|
2
2
|
import { normalizeCalendarUrlSegment } from "@remit/data-ports/id";
|
|
3
|
-
import { CalendarSource } from "@remit/domain-enums";
|
|
3
|
+
import { CalendarComponentSet, CalendarSource } from "@remit/domain-enums";
|
|
4
4
|
import { defaultErrorCode } from "../error.js";
|
|
5
5
|
export const refuseCalendar = (code, message) => ({ ok: false, error: { code, message } });
|
|
6
6
|
/**
|
|
@@ -36,6 +36,11 @@ export const calendarDepsOf = (client) => ({
|
|
|
36
36
|
calendarEventIndex: client.calendarEventIndex,
|
|
37
37
|
calendarFeedToken: client.calendarFeedToken,
|
|
38
38
|
calendarStore: client.calendarStore,
|
|
39
|
+
calendarTask: client.calendarTask,
|
|
40
|
+
accounts: client.account,
|
|
41
|
+
accountSetting: client.accountSetting,
|
|
42
|
+
outboxMessage: client.outboxMessage,
|
|
43
|
+
outboxSchedulingPart: client.outboxSchedulingPart,
|
|
39
44
|
});
|
|
40
45
|
const toCalendarResponse = (item) => ({
|
|
41
46
|
calendarId: item.calendarId,
|
|
@@ -66,9 +71,15 @@ const toCalendarResponse = (item) => ({
|
|
|
66
71
|
*/
|
|
67
72
|
export const listCalendarsFor = async (deps, accountConfigId) => {
|
|
68
73
|
const existing = await deps.calendarCollection.listByAccountConfig(accountConfigId);
|
|
69
|
-
|
|
74
|
+
const hasEvents = existing.some((collection) => collection.componentSet === CalendarComponentSet.VeventOnly);
|
|
75
|
+
const hasTasks = existing.some((collection) => collection.componentSet === CalendarComponentSet.VtodoOnly);
|
|
76
|
+
if (hasEvents && hasTasks)
|
|
70
77
|
return existing;
|
|
71
|
-
|
|
78
|
+
if (!hasEvents) {
|
|
79
|
+
await provisionDefaultCalendar(deps.calendarStore, accountConfigId);
|
|
80
|
+
}
|
|
81
|
+
if (!hasTasks)
|
|
82
|
+
await provisionTaskCalendar(deps.calendarStore, accountConfigId);
|
|
72
83
|
return deps.calendarCollection.listByAccountConfig(accountConfigId);
|
|
73
84
|
};
|
|
74
85
|
/** One of the caller's collections, or a refusal naming the one they asked for. */
|
|
@@ -90,6 +101,9 @@ export const findWritableCalendarFor = async (deps, accountConfigId, calendarId)
|
|
|
90
101
|
const found = await findCalendarFor(deps, accountConfigId, calendarId);
|
|
91
102
|
if (!found.ok)
|
|
92
103
|
return found;
|
|
104
|
+
if (found.value.componentSet === CalendarComponentSet.VtodoOnly) {
|
|
105
|
+
return refuseCalendar("NotAnEventCalendar", `"${found.value.displayName}" holds tasks, so an event cannot be written into it`);
|
|
106
|
+
}
|
|
93
107
|
if (found.value.source === CalendarSource.Subscribed) {
|
|
94
108
|
return refuseCalendar("ReadOnlyCalendar", `"${found.value.displayName}" is subscribed to a feed and is read-only — change the event where the feed comes from`);
|
|
95
109
|
}
|
|
@@ -117,12 +117,20 @@ export declare const handlers: {
|
|
|
117
117
|
TrashOperations_emptyTrash: (context: import("openapi-backend").Context<any, any, any, any, any, import("openapi-backend").Document>, ctx: import("../types.js").HandlerContext<"sync">) => Promise<{
|
|
118
118
|
deletedCount: number;
|
|
119
119
|
}>;
|
|
120
|
+
CalendarTaskDetailOperations_getCalendarTask: OperationHandler<import("../types.js").CalendarTaskDetailOperationIds>;
|
|
121
|
+
CalendarTaskDetailOperations_updateCalendarTask: OperationHandler<import("../types.js").CalendarTaskDetailOperationIds>;
|
|
122
|
+
CalendarTaskDetailOperations_deleteCalendarTask: OperationHandler<import("../types.js").CalendarTaskDetailOperationIds>;
|
|
123
|
+
CalendarTaskOperations_listCalendarTasks: OperationHandler<import("../types.js").CalendarTaskOperationIds>;
|
|
124
|
+
CalendarTaskOperations_createCalendarTask: OperationHandler<import("../types.js").CalendarTaskOperationIds>;
|
|
125
|
+
UserSettingsOperations_getUserSettings: OperationHandler<import("../types.js").UserSettingsOperationIds>;
|
|
126
|
+
UserSettingsOperations_updateUserSettings: OperationHandler<import("../types.js").UserSettingsOperationIds>;
|
|
127
|
+
UserSettingsOperations_clearDefaultInvitationSender: OperationHandler<import("../types.js").UserSettingsOperationIds>;
|
|
120
128
|
CalendarFreeBusyOperations_listCalendarFreeBusy: OperationHandler<"CalendarFreeBusyOperations_listCalendarFreeBusy">;
|
|
121
|
-
CalendarEventDetailOperations_getCalendarEvent: OperationHandler<import("../types.js").CalendarEventDetailOperationIds>;
|
|
122
|
-
CalendarEventDetailOperations_updateCalendarEvent: OperationHandler<import("../types.js").CalendarEventDetailOperationIds>;
|
|
123
|
-
CalendarEventDetailOperations_deleteCalendarEvent: OperationHandler<import("../types.js").CalendarEventDetailOperationIds>;
|
|
124
|
-
CalendarEventOperations_listCalendarEvents: OperationHandler<import("../types.js").CalendarEventOperationIds>;
|
|
125
|
-
CalendarEventOperations_createCalendarEvent: OperationHandler<import("../types.js").CalendarEventOperationIds>;
|
|
129
|
+
CalendarEventDetailOperations_getCalendarEvent: OperationHandler<import("../types.js").CalendarEventDetailOperationIds, "smtp">;
|
|
130
|
+
CalendarEventDetailOperations_updateCalendarEvent: OperationHandler<import("../types.js").CalendarEventDetailOperationIds, "smtp">;
|
|
131
|
+
CalendarEventDetailOperations_deleteCalendarEvent: OperationHandler<import("../types.js").CalendarEventDetailOperationIds, "smtp">;
|
|
132
|
+
CalendarEventOperations_listCalendarEvents: OperationHandler<import("../types.js").CalendarEventOperationIds, "smtp">;
|
|
133
|
+
CalendarEventOperations_createCalendarEvent: OperationHandler<import("../types.js").CalendarEventOperationIds, "smtp">;
|
|
126
134
|
CalendarDetailOperations_getCalendarFeed: OperationHandler<import("../types.js").CalendarDetailOperationIds>;
|
|
127
135
|
CalendarDetailOperations_putCalendarFeed: OperationHandler<import("../types.js").CalendarDetailOperationIds>;
|
|
128
136
|
CalendarDetailOperations_revokeCalendarFeed: OperationHandler<import("../types.js").CalendarDetailOperationIds>;
|
|
@@ -5,6 +5,7 @@ import { CalendarDetailOperations, CalendarOperations } from "./calendar.js";
|
|
|
5
5
|
import { CalendarEventDetailOperations, CalendarEventOperations, CalendarFreeBusyOperations, } from "./calendar-event.js";
|
|
6
6
|
import { CalendarFeedDetailOperations, getCalendarFeedIcal, } from "./calendar-feed.js";
|
|
7
7
|
import { CalendarSuggestionActionOperations, CalendarSuggestionOperations, MessageCalendarSuggestionOperations, } from "./calendar-suggestion.js";
|
|
8
|
+
import { CalendarTaskDetailOperations, CalendarTaskOperations, } from "./calendar-task.js";
|
|
8
9
|
import { ConfigOperations } from "./config.js";
|
|
9
10
|
import { FilterDetailOperations, FilterOperations } from "./filter.js";
|
|
10
11
|
import { FolderRoleOperations } from "./folder-role.js";
|
|
@@ -18,6 +19,7 @@ import { SyncOperations } from "./sync.js";
|
|
|
18
19
|
import { SystemOperations } from "./system-update.js";
|
|
19
20
|
import { ThreadDetailOperations, ThreadOperations } from "./thread.js";
|
|
20
21
|
import { UnifiedThreadOperations } from "./unified-threads.js";
|
|
22
|
+
import { UserSettingsOperations } from "./user-settings.js";
|
|
21
23
|
export const handlers = {
|
|
22
24
|
...MeOperations,
|
|
23
25
|
...ConfigOperations,
|
|
@@ -40,6 +42,9 @@ export const handlers = {
|
|
|
40
42
|
...CalendarEventOperations,
|
|
41
43
|
...CalendarEventDetailOperations,
|
|
42
44
|
...CalendarFreeBusyOperations,
|
|
45
|
+
...UserSettingsOperations,
|
|
46
|
+
...CalendarTaskOperations,
|
|
47
|
+
...CalendarTaskDetailOperations,
|
|
43
48
|
...TrashOperations,
|
|
44
49
|
...SyncOperations,
|
|
45
50
|
...ThreadDetailOperations,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UpdateUserSettingsInput, UserSettingsResponse } from "@remit/api-openapi-types";
|
|
2
|
+
import type { IAccountRepository, IAccountSettingRepository } from "@remit/data-ports";
|
|
3
|
+
import type { OperationHandler, UserSettingsOperationIds } from "../types.js";
|
|
4
|
+
export interface UserSettingsDeps {
|
|
5
|
+
accountSetting: Pick<IAccountSettingRepository, "get" | "upsert" | "delete">;
|
|
6
|
+
accounts: Pick<IAccountRepository, "listAllByAccountConfig">;
|
|
7
|
+
}
|
|
8
|
+
/** The account the reader chose to send invitations from, if they chose one. */
|
|
9
|
+
export declare const chosenInvitationSender: (accountSetting: Pick<IAccountSettingRepository, "get">, accountConfigId: string) => Promise<string | undefined>;
|
|
10
|
+
export declare const readUserSettings: (deps: UserSettingsDeps, accountConfigId: string) => Promise<UserSettingsResponse>;
|
|
11
|
+
export type UserSettingsOutcome = {
|
|
12
|
+
ok: true;
|
|
13
|
+
value: UserSettingsResponse;
|
|
14
|
+
} | {
|
|
15
|
+
ok: false;
|
|
16
|
+
error: {
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare const updateUserSettingsFor: (deps: UserSettingsDeps, accountConfigId: string, input: UpdateUserSettingsInput) => Promise<UserSettingsOutcome>;
|
|
22
|
+
export declare const UserSettingsOperations: Record<UserSettingsOperationIds, OperationHandler<UserSettingsOperationIds>>;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { invitationSenderInEffect, sendingAccountFor, } from "@remit/calendar-service";
|
|
2
|
+
import { parseAccountSettingValue } from "@remit/data-ports/account-settings";
|
|
3
|
+
import { AccountSettingName } from "@remit/domain-enums";
|
|
4
|
+
import { badRequest } from "./calendar.js";
|
|
5
|
+
/** The account the reader chose to send invitations from, if they chose one. */
|
|
6
|
+
export const chosenInvitationSender = async (accountSetting, accountConfigId) => {
|
|
7
|
+
const stored = await accountSetting.get(accountConfigId, AccountSettingName.DefaultInvitationSenderAccountId);
|
|
8
|
+
return stored
|
|
9
|
+
? parseAccountSettingValue(AccountSettingName.DefaultInvitationSenderAccountId, stored.value).value
|
|
10
|
+
: undefined;
|
|
11
|
+
};
|
|
12
|
+
export const readUserSettings = async (deps, accountConfigId) => {
|
|
13
|
+
const chosen = await chosenInvitationSender(deps.accountSetting, accountConfigId);
|
|
14
|
+
const inEffect = invitationSenderInEffect(await deps.accounts.listAllByAccountConfig(accountConfigId), chosen);
|
|
15
|
+
return {
|
|
16
|
+
...(inEffect && { defaultInvitationSenderAccountId: inEffect.accountId }),
|
|
17
|
+
defaultInvitationSenderChosen: inEffect !== undefined && inEffect.accountId === chosen,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export const updateUserSettingsFor = async (deps, accountConfigId, input) => {
|
|
21
|
+
const sender = input.defaultInvitationSenderAccountId;
|
|
22
|
+
if (sender !== undefined) {
|
|
23
|
+
const account = sendingAccountFor(await deps.accounts.listAllByAccountConfig(accountConfigId), sender);
|
|
24
|
+
if (!account.ok) {
|
|
25
|
+
return {
|
|
26
|
+
ok: false,
|
|
27
|
+
error: { code: "UnknownSender", message: account.error.message },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
await deps.accountSetting.upsert({
|
|
31
|
+
accountConfigId,
|
|
32
|
+
name: AccountSettingName.DefaultInvitationSenderAccountId,
|
|
33
|
+
value: { kind: "String", value: sender },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return { ok: true, value: await readUserSettings(deps, accountConfigId) };
|
|
37
|
+
};
|
|
38
|
+
const depsOf = (ctx) => ({
|
|
39
|
+
accountSetting: ctx.client.accountSetting,
|
|
40
|
+
accounts: ctx.client.account,
|
|
41
|
+
});
|
|
42
|
+
export const UserSettingsOperations = {
|
|
43
|
+
UserSettingsOperations_getUserSettings: async (_context, ctx) => readUserSettings(depsOf(ctx), ctx.identity.accountConfigId),
|
|
44
|
+
UserSettingsOperations_updateUserSettings: async (context, ctx) => {
|
|
45
|
+
const updated = await updateUserSettingsFor(depsOf(ctx), ctx.identity.accountConfigId, context.request.requestBody);
|
|
46
|
+
if (!updated.ok)
|
|
47
|
+
return badRequest(updated.error);
|
|
48
|
+
return updated.value;
|
|
49
|
+
},
|
|
50
|
+
UserSettingsOperations_clearDefaultInvitationSender: async (_context, ctx) => {
|
|
51
|
+
const deps = depsOf(ctx);
|
|
52
|
+
await deps.accountSetting.delete(ctx.identity.accountConfigId, AccountSettingName.DefaultInvitationSenderAccountId);
|
|
53
|
+
return readUserSettings(deps, ctx.identity.accountConfigId);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IAccountConfigRepository, IAccountExportRequestRepository, IAccountRepository, IAccountSettingRepository, IAddressRepository, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, ICalendarSuggestionRepository, IConfigImportRepository, IEnvelopeRepository, IFilterRepository, ILabelRepository, IMailboxLockRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageLabelRepository, IMessagePlacementMoveRepository, IMessageRepository, IOrganizeJobRequestRepository, IOutboxAttachmentRepository, IOutboxMessageRepository, IQuarantineRepository, ISenderSignerStandingRepository, ISyncWarningRepository, IThreadMessageRepository } from "@remit/data-ports";
|
|
1
|
+
import type { IAccountConfigRepository, IAccountExportRequestRepository, IAccountRepository, IAccountSettingRepository, IAddressRepository, ICalendarCollectionRepository, ICalendarEventIndexRepository, ICalendarFeedTokenRepository, ICalendarObjectRepository, ICalendarStore, ICalendarSuggestionRepository, ICalendarTaskRepository, IConfigImportRepository, IEnvelopeRepository, IFilterRepository, ILabelRepository, IMailboxLockRepository, IMailboxRepository, IMailboxSpecialUseRepository, IMessageFlagPushRepository, IMessageFlagRepository, IMessageLabelRepository, IMessagePlacementMoveRepository, IMessageRepository, IOrganizeJobRequestRepository, IOutboxAttachmentRepository, IOutboxMessageRepository, IOutboxSchedulingPartRepository, IQuarantineRepository, ISenderSignerStandingRepository, ISyncWarningRepository, IThreadMessageRepository } from "@remit/data-ports";
|
|
2
2
|
import { BodySyncQueueService, BodySyncService, type ConnectionScope, type FilterConfig, FlagQueueService, type ImapTransport, MailboxQueueService, MessageMoveService, OutboxAttachmentService, OutboxQueueService, SpamReportService } from "@remit/mailbox-service";
|
|
3
3
|
import { type SecretsService } from "@remit/secrets-service";
|
|
4
4
|
import type { SmtpGetSocket } from "@remit/smtp-service";
|
|
@@ -21,6 +21,7 @@ export interface RemitClientCore {
|
|
|
21
21
|
message: IMessageRepository;
|
|
22
22
|
messageFlag: IMessageFlagRepository;
|
|
23
23
|
outboxMessage: IOutboxMessageRepository;
|
|
24
|
+
outboxSchedulingPart: IOutboxSchedulingPartRepository;
|
|
24
25
|
threadMessage: IThreadMessageRepository;
|
|
25
26
|
envelope: IEnvelopeRepository;
|
|
26
27
|
accountExportRequest: IAccountExportRequestRepository;
|
|
@@ -38,6 +39,7 @@ export interface RemitClientCore {
|
|
|
38
39
|
calendarFeedToken: ICalendarFeedTokenRepository;
|
|
39
40
|
calendarSuggestion: ICalendarSuggestionRepository;
|
|
40
41
|
calendarStore: ICalendarStore;
|
|
42
|
+
calendarTask: ICalendarTaskRepository;
|
|
41
43
|
storage: StorageService;
|
|
42
44
|
secrets: SecretsService;
|
|
43
45
|
placementMove: IMessagePlacementMoveRepository;
|
|
@@ -75,6 +77,7 @@ export interface RemitClientRepositories {
|
|
|
75
77
|
message: IMessageRepository;
|
|
76
78
|
messageFlag: IMessageFlagRepository;
|
|
77
79
|
outboxMessage: IOutboxMessageRepository;
|
|
80
|
+
outboxSchedulingPart: IOutboxSchedulingPartRepository;
|
|
78
81
|
outboxAttachment: IOutboxAttachmentRepository;
|
|
79
82
|
threadMessage: IThreadMessageRepository;
|
|
80
83
|
envelope: IEnvelopeRepository;
|
|
@@ -95,6 +98,7 @@ export interface RemitClientRepositories {
|
|
|
95
98
|
calendarFeedToken: ICalendarFeedTokenRepository;
|
|
96
99
|
calendarSuggestion: ICalendarSuggestionRepository;
|
|
97
100
|
calendarStore: ICalendarStore;
|
|
101
|
+
calendarTask: ICalendarTaskRepository;
|
|
98
102
|
}
|
|
99
103
|
export interface RemitClientSharedDeps {
|
|
100
104
|
storage: StorageService;
|
|
@@ -57,6 +57,7 @@ export const createRemitCore = (deps) => {
|
|
|
57
57
|
message: repositories.message,
|
|
58
58
|
messageFlag: repositories.messageFlag,
|
|
59
59
|
outboxMessage: repositories.outboxMessage,
|
|
60
|
+
outboxSchedulingPart: repositories.outboxSchedulingPart,
|
|
60
61
|
threadMessage: repositories.threadMessage,
|
|
61
62
|
envelope: repositories.envelope,
|
|
62
63
|
accountExportRequest: repositories.accountExportRequest,
|
|
@@ -74,6 +75,7 @@ export const createRemitCore = (deps) => {
|
|
|
74
75
|
calendarFeedToken: repositories.calendarFeedToken,
|
|
75
76
|
calendarSuggestion: repositories.calendarSuggestion,
|
|
76
77
|
calendarStore: repositories.calendarStore,
|
|
78
|
+
calendarTask: repositories.calendarTask,
|
|
77
79
|
storage,
|
|
78
80
|
secrets,
|
|
79
81
|
placementMove: repositories.placementMove,
|
|
@@ -155,6 +157,7 @@ export const composeReadPathServices = (core, queues, sync) => ({
|
|
|
155
157
|
calendarSuggestionService: core.calendarSuggestion,
|
|
156
158
|
calendarStore: core.calendarStore,
|
|
157
159
|
filterService: core.filter,
|
|
160
|
+
accountService: core.account,
|
|
158
161
|
}),
|
|
159
162
|
});
|
|
160
163
|
export const createRemitClient = (deps) => {
|
package/dist/src/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ApiConfig } from "./config/api-config.js";
|
|
|
3
3
|
import type { RemitClient } from "./service/create-remit-client.js";
|
|
4
4
|
import type { QueueName } from "./service/queues.js";
|
|
5
5
|
type MatchPrefix<Prefix extends string, S extends string> = S extends `${Prefix}${infer _}` ? S : never;
|
|
6
|
-
export type OperationIds = "MeOperations_deleteMe" | "MeOperations_listVipSuggestions" | "MeOperations_createExport" | "MeOperations_getExport" | "MeOperations_listQuarantine" | "ConfigOperations_getConfig" | "ConfigOperations_exportConfig" | "ConfigOperations_importConfig" | "SystemOperations_getSystemUpdate" | "SystemOperations_applySystemUpdate" | "AccountOperations_createAccount" | "AccountOperations_testConnection" | "AccountDetailOperations_updateAccount" | "AccountDetailOperations_deleteAccount" | "MicrosoftOAuthOperations_microsoftOAuthStart" | "MicrosoftOAuthOperations_microsoftOAuthCallback" | "MailboxOperations_listMailboxes" | "MailboxOperations_createMailbox" | "MailboxDetailOperations_getMailbox" | "MailboxDetailOperations_renameMailbox" | "MailboxDetailOperations_deleteMailbox" | "FolderRoleOperations_appointFolderRole" | "FilterOperations_listFilters" | "FilterOperations_createFilter" | "FilterDetailOperations_getFilter" | "FilterDetailOperations_updateFilter" | "FilterDetailOperations_deleteFilter" | "LabelOperations_listLabels" | "LabelOperations_createLabel" | "LabelDetailOperations_getLabel" | "LabelDetailOperations_updateLabel" | "LabelDetailOperations_deleteLabel" | "OrganizeOperations_createOrganizeJob" | "OrganizeOperations_previewOrganize" | "OrganizeJobDetailOperations_getOrganizeJob" | "SyncOperations_triggerSync" | "SyncOperations_getSyncStatus" | "ThreadDetailOperations_listThreadMessages" | "UnifiedThreadOperations_listAllThreads" | "ThreadOperations_listThreads" | "ThreadOperations_searchThreads" | "MessageOperations_describeMessage" | "MessageOperations_getRawMessage" | "MessageOperations_updateMessageFlags" | "MessageBulkOperations_deleteMessages" | "MessageBulkOperations_moveMessages" | "MessageBulkOperations_updateFlags" | "MessageBulkOperations_copyMessages" | "MessageBulkOperations_updateMessageLabels" | "MessageBulkOperations_reportSpam" | "MessageBulkOperations_notSpam" | "CalendarOperations_listCalendars" | "CalendarOperations_createCalendar" | "CalendarDetailOperations_getCalendar" | "CalendarDetailOperations_updateCalendar" | "CalendarDetailOperations_deleteCalendar" | "CalendarDetailOperations_getCalendarFeed" | "CalendarDetailOperations_putCalendarFeed" | "CalendarDetailOperations_revokeCalendarFeed" | "CalendarFeedOperations_getCalendarFeedIcal" | "CalendarEventOperations_listCalendarEvents" | "CalendarEventOperations_createCalendarEvent" | "CalendarEventDetailOperations_getCalendarEvent" | "CalendarEventDetailOperations_updateCalendarEvent" | "CalendarEventDetailOperations_deleteCalendarEvent" | "CalendarFreeBusyOperations_listCalendarFreeBusy" | "TrashOperations_emptyTrash" | "OutboxOperations_createOutboxMessage" | "OutboxOperations_listOutboxMessages" | "OutboxDetailOperations_getOutboxMessage" | "OutboxDetailOperations_updateOutboxMessage" | "OutboxDetailOperations_deleteOutboxMessage" | "OutboxDetailOperations_sendOutboxMessage" | "OutboxDetailOperations_mintOutboxAttachment" | "OutboxAttachmentOperations_completeOutboxAttachment" | "AddressOperations_searchAddresses" | "AddressDetailOperations_updateAddress" | "CalendarSuggestionOperations_listCalendarSuggestions" | "MessageCalendarSuggestionOperations_listMessageCalendarSuggestions" | "CalendarSuggestionActionOperations_acceptCalendarSuggestion" | "CalendarSuggestionActionOperations_declineCalendarSuggestion" | "CalendarSuggestionActionOperations_dismissCalendarSuggestion" | "CalendarSuggestionActionOperations_reopenCalendarSuggestion";
|
|
6
|
+
export type OperationIds = "MeOperations_deleteMe" | "MeOperations_listVipSuggestions" | "MeOperations_createExport" | "MeOperations_getExport" | "MeOperations_listQuarantine" | "ConfigOperations_getConfig" | "ConfigOperations_exportConfig" | "ConfigOperations_importConfig" | "SystemOperations_getSystemUpdate" | "SystemOperations_applySystemUpdate" | "AccountOperations_createAccount" | "AccountOperations_testConnection" | "AccountDetailOperations_updateAccount" | "AccountDetailOperations_deleteAccount" | "MicrosoftOAuthOperations_microsoftOAuthStart" | "MicrosoftOAuthOperations_microsoftOAuthCallback" | "MailboxOperations_listMailboxes" | "MailboxOperations_createMailbox" | "MailboxDetailOperations_getMailbox" | "MailboxDetailOperations_renameMailbox" | "MailboxDetailOperations_deleteMailbox" | "FolderRoleOperations_appointFolderRole" | "FilterOperations_listFilters" | "FilterOperations_createFilter" | "FilterDetailOperations_getFilter" | "FilterDetailOperations_updateFilter" | "FilterDetailOperations_deleteFilter" | "LabelOperations_listLabels" | "LabelOperations_createLabel" | "LabelDetailOperations_getLabel" | "LabelDetailOperations_updateLabel" | "LabelDetailOperations_deleteLabel" | "OrganizeOperations_createOrganizeJob" | "OrganizeOperations_previewOrganize" | "OrganizeJobDetailOperations_getOrganizeJob" | "SyncOperations_triggerSync" | "SyncOperations_getSyncStatus" | "ThreadDetailOperations_listThreadMessages" | "UnifiedThreadOperations_listAllThreads" | "ThreadOperations_listThreads" | "ThreadOperations_searchThreads" | "MessageOperations_describeMessage" | "MessageOperations_getRawMessage" | "MessageOperations_updateMessageFlags" | "MessageBulkOperations_deleteMessages" | "MessageBulkOperations_moveMessages" | "MessageBulkOperations_updateFlags" | "MessageBulkOperations_copyMessages" | "MessageBulkOperations_updateMessageLabels" | "MessageBulkOperations_reportSpam" | "MessageBulkOperations_notSpam" | "CalendarOperations_listCalendars" | "CalendarOperations_createCalendar" | "CalendarDetailOperations_getCalendar" | "CalendarDetailOperations_updateCalendar" | "CalendarDetailOperations_deleteCalendar" | "CalendarDetailOperations_getCalendarFeed" | "CalendarDetailOperations_putCalendarFeed" | "CalendarDetailOperations_revokeCalendarFeed" | "CalendarFeedOperations_getCalendarFeedIcal" | "CalendarEventOperations_listCalendarEvents" | "CalendarEventOperations_createCalendarEvent" | "CalendarEventDetailOperations_getCalendarEvent" | "CalendarEventDetailOperations_updateCalendarEvent" | "CalendarEventDetailOperations_deleteCalendarEvent" | "CalendarFreeBusyOperations_listCalendarFreeBusy" | "UserSettingsOperations_getUserSettings" | "UserSettingsOperations_updateUserSettings" | "UserSettingsOperations_clearDefaultInvitationSender" | "CalendarTaskOperations_listCalendarTasks" | "CalendarTaskOperations_createCalendarTask" | "CalendarTaskDetailOperations_getCalendarTask" | "CalendarTaskDetailOperations_updateCalendarTask" | "CalendarTaskDetailOperations_deleteCalendarTask" | "TrashOperations_emptyTrash" | "OutboxOperations_createOutboxMessage" | "OutboxOperations_listOutboxMessages" | "OutboxDetailOperations_getOutboxMessage" | "OutboxDetailOperations_updateOutboxMessage" | "OutboxDetailOperations_deleteOutboxMessage" | "OutboxDetailOperations_sendOutboxMessage" | "OutboxDetailOperations_mintOutboxAttachment" | "OutboxAttachmentOperations_completeOutboxAttachment" | "AddressOperations_searchAddresses" | "AddressDetailOperations_updateAddress" | "CalendarSuggestionOperations_listCalendarSuggestions" | "MessageCalendarSuggestionOperations_listMessageCalendarSuggestions" | "CalendarSuggestionActionOperations_acceptCalendarSuggestion" | "CalendarSuggestionActionOperations_declineCalendarSuggestion" | "CalendarSuggestionActionOperations_dismissCalendarSuggestion" | "CalendarSuggestionActionOperations_reopenCalendarSuggestion";
|
|
7
7
|
export type MeOperationIds = MatchPrefix<"MeOperations_", OperationIds>;
|
|
8
8
|
export type ConfigOperationIds = MatchPrefix<"ConfigOperations_", OperationIds>;
|
|
9
9
|
export type SystemOperationIds = MatchPrefix<"SystemOperations_", OperationIds>;
|
|
@@ -44,6 +44,9 @@ export type OutboxAttachmentOperationIds = MatchPrefix<"OutboxAttachmentOperatio
|
|
|
44
44
|
export type AddressOperationIds = MatchPrefix<"AddressOperations_", OperationIds>;
|
|
45
45
|
export type AddressDetailOperationIds = MatchPrefix<"AddressDetailOperations_", OperationIds>;
|
|
46
46
|
export type MicrosoftOAuthOperationIds = MatchPrefix<"MicrosoftOAuthOperations_", OperationIds>;
|
|
47
|
+
export type UserSettingsOperationIds = MatchPrefix<"UserSettingsOperations_", OperationIds>;
|
|
48
|
+
export type CalendarTaskOperationIds = MatchPrefix<"CalendarTaskOperations_", OperationIds>;
|
|
49
|
+
export type CalendarTaskDetailOperationIds = MatchPrefix<"CalendarTaskDetailOperations_", OperationIds>;
|
|
47
50
|
export type CalendarSuggestionOperationIds = MatchPrefix<"CalendarSuggestionOperations_", OperationIds>;
|
|
48
51
|
export type MessageCalendarSuggestionOperationIds = MatchPrefix<"MessageCalendarSuggestionOperations_", OperationIds>;
|
|
49
52
|
export type CalendarSuggestionActionOperationIds = MatchPrefix<"CalendarSuggestionActionOperations_", OperationIds>;
|