@remit/backend 0.0.157 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- /** Mints the id a new resource is named and identified by. */
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
- /** The collections a listing covers, defaulting to every one the caller holds. */
27
- export declare const collectionsForListing: (deps: CalendarDeps, accountConfigId: string, calendarIds: string[]) => Promise<CalendarOutcome<CalendarCollectionItem[]>>;
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
- export declare const createCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, input: CreateCalendarEventInput) => Promise<CalendarOutcome<CalendarObjectItem>>;
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
- export declare const updateCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, request: ScopedRequest, patch: Partial<CalendarEventFields>) => Promise<CalendarOutcome<CalendarObjectItem | null>>;
67
- export declare const deleteCalendarEventFor: (deps: CalendarEventDeps, accountConfigId: string, request: ScopedRequest) => Promise<CalendarOutcome<CalendarObjectItem | null>>;
68
- export declare const CalendarEventOperations: Record<CalendarEventOperationIds, OperationHandler<CalendarEventOperationIds>>;
69
- export declare const CalendarEventDetailOperations: Record<CalendarEventDetailOperationIds, OperationHandler<CalendarEventDetailOperationIds>>;
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
- /** The collections a listing covers, defaulting to every one the caller holds. */
45
- export const collectionsForListing = async (deps, accountConfigId, calendarIds) => {
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 { ok: true, value: held };
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 toEventResponse = (object) => ({
72
- calendarObjectId: object.calendarObjectId,
73
- calendarId: object.calendarId,
74
- resourceName: object.resourceName,
75
- icalUid: object.icalUid,
76
- icalData: object.icalData,
77
- etag: object.etag,
78
- sequence: object.sequence,
79
- syncSequence: object.syncSequence,
80
- summary: object.summary,
81
- dtStart: object.dtStart,
82
- dtEnd: object.dtEnd,
83
- allDay: object.allDay,
84
- zoneCertainty: object.zoneCertainty,
85
- status: object.status,
86
- transparency: object.transparency,
87
- hasRecurrence: object.hasRecurrence,
88
- expandedThrough: object.expandedThrough,
89
- createdAt: object.createdAt,
90
- updatedAt: object.updatedAt,
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
- * Writes what a scoped edit resolved to.
225
+ * Turns what a scoped edit resolved to into the store writes it is.
165
226
  *
166
- * A split is two resources and one write set: the truncated original and the
167
- * remainder land together or not at all, because half a split is a series the
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
- export const commitScopedWrite = async (deps, accountConfigId, object, collectionTimezone, write) => {
171
- if (write.kind === "Delete") {
172
- await deleteCalendarObject(deps.calendarStore, {
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
- await deps.calendarStore.commit([head.value]);
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: await readWrittenObject(deps.calendarStore, following.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
- return putCalendarObject(deps.calendarStore, {
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: built.value,
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
- export const updateCalendarEventFor = async (deps, accountConfigId, request, patch) => {
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 refuseCalendar("EtagMismatch", "this event has been written since you read it — read it again and reapply the change");
248
- }
249
- const parsed = await parseCalendar(object.icalData);
250
- if (!parsed.ok) {
251
- throw new Error(`stored calendar object ${object.calendarObjectId} no longer parses: ${parsed.error.message}`);
252
- }
253
- const write = await applyScopedUpdate(parsed.value, collection.timezone, {
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
- return commitScopedWrite(deps, accountConfigId, object, collection.timezone, write.value);
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 refuseCalendar("EtagMismatch", "this event has been written since you read it — read it again and reapply the change");
269
- }
270
- const parsed = await parseCalendar(object.icalData);
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
- return commitScopedWrite(deps, accountConfigId, object, collection.timezone, write.value);
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 = calendarEventDepsOf(calendarDepsOf(ctx.client));
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 = calendarEventDepsOf(calendarDepsOf(ctx.client));
361
- if (request.value.calendarId === "") {
362
- const located = await locateResource(deps, await listCalendarsFor(deps, accountConfigId), request.value.calendarObjectId);
363
- if (!located) {
364
- return notFound(`no event ${request.value.calendarObjectId} on this account`);
365
- }
366
- return toEventResponse(located.object);
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 = await resolveResource(deps, accountConfigId, request.value, "read");
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 = calendarEventDepsOf(calendarDepsOf(ctx.client));
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 deps = calendarEventDepsOf(calendarDepsOf(ctx.client));
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: 204 };
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
- if (existing.length > 0)
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
- await provisionDefaultCalendar(deps.calendarStore, accountConfigId);
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) => {
@@ -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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remit/backend",
3
- "version": "0.0.157",
3
+ "version": "1.0.1",
4
4
  "description": "Remit Mail Inspector API backend",
5
5
  "license": "MIT",
6
6
  "author": "",