@dereekb/firebase 13.40.0 → 13.42.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/eslint/index.esm.js +145 -170
- package/eslint/package.json +3 -3
- package/index.esm.js +2865 -551
- package/package.json +5 -5
- package/src/lib/common/firestore/snapshot/snapshot.field.d.ts +4 -0
- package/src/lib/common/storage/context.d.ts +9 -1
- package/src/lib/common/storage/driver/accessor.d.ts +12 -0
- package/src/lib/common/storage/index.d.ts +1 -0
- package/src/lib/common/storage/storage.url.d.ts +69 -0
- package/src/lib/model/calendar/calendar.action.d.ts +34 -0
- package/src/lib/model/calendar/calendar.api.d.ts +147 -0
- package/src/lib/model/calendar/calendar.api.error.d.ts +24 -0
- package/src/lib/model/calendar/calendar.d.ts +460 -0
- package/src/lib/model/calendar/calendar.expand.d.ts +95 -0
- package/src/lib/model/calendar/calendar.ics.d.ts +322 -0
- package/src/lib/model/calendar/calendar.id.d.ts +110 -0
- package/src/lib/model/calendar/calendar.processing.d.ts +77 -0
- package/src/lib/model/calendar/calendar.query.d.ts +76 -0
- package/src/lib/model/calendar/calendar.schedule.d.ts +82 -0
- package/src/lib/model/calendar/calendar.type.d.ts +188 -0
- package/src/lib/model/calendar/calendar.util.d.ts +485 -0
- package/src/lib/model/calendar/index.d.ts +12 -0
- package/src/lib/model/index.d.ts +1 -0
- package/src/lib/model/notification/notification.message.d.ts +83 -0
- package/src/lib/model/notification/notification.query.d.ts +41 -0
- package/src/lib/model/oidcmodel/oidcmodel.query.d.ts +36 -0
- package/src/lib/model/storagefile/storagefile.api.d.ts +23 -2
- package/src/lib/model/storagefile/storagefile.create.d.ts +14 -3
- package/src/lib/model/storagefile/storagefile.query.d.ts +36 -0
- package/src/lib/model/system/index.d.ts +1 -0
- package/src/lib/model/system/system.scheduler.d.ts +235 -0
- package/test/index.esm.js +24 -1
- package/test/package.json +6 -6
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { type ArrayOrValue, type Maybe, type Minutes, type TimezoneString, type WebsiteUrl } from '@dereekb/util';
|
|
2
|
+
import { type DateRange, type ICalendar, type ICalendarAttendee, type ICalendarAttendeeRole, type ICalendarEvent, type ICalendarExtraProperty, type ICalendarIcsString, type ICalendarMethod, type ICalendarOrganizer, type ICalendarParticipationStatus, type ICalendarSerializeConfig, type ICalendarUid, type ICalendarUidFactory } from '@dereekb/date';
|
|
3
|
+
import { type Calendar, type CalendarEventItem, type CalendarRecurringEventItem } from './calendar';
|
|
4
|
+
import { type CalendarExtensionData, type CalendarId } from './calendar.id';
|
|
5
|
+
import { type CalendarIcsRecurrenceMode, type CalendarTypeConfig } from './calendar.type';
|
|
6
|
+
import { type CalendarEventOccurrence } from './calendar.expand';
|
|
7
|
+
/**
|
|
8
|
+
* @module calendar.ics
|
|
9
|
+
*
|
|
10
|
+
* Renders a {@link Calendar} into the `@dereekb/date` iCalendar model, and from there into an ICS document.
|
|
11
|
+
*
|
|
12
|
+
* The output is DETERMINISTIC for a fixed `now`: every property is emitted in a fixed order and no object
|
|
13
|
+
* keys are iterated in insertion order. That is what lets the publisher content-hash a payload and skip a
|
|
14
|
+
* no-op upload, and it is why the processor passes `now: calendar.uat` rather than `new Date()` — DTSTAMP
|
|
15
|
+
* then moves only when the content moves.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The "X-" prefix an extension property name is given when it does not already carry one.
|
|
19
|
+
*/
|
|
20
|
+
export declare const CALENDAR_EXTENSION_PROPERTY_PREFIX = "X-";
|
|
21
|
+
/**
|
|
22
|
+
* Converts a calendar's or event's {@link CalendarExtensionData} into iCalendar extra properties.
|
|
23
|
+
*
|
|
24
|
+
* The stored key is a SUFFIX: it is uppercased, stripped of anything outside `[A-Z0-9-]`, and prefixed with
|
|
25
|
+
* "X-" unless it already carries one. Storing the suffix and prefixing at emit time is what makes it
|
|
26
|
+
* impossible for stored data to shadow a standard property such as SUMMARY.
|
|
27
|
+
*
|
|
28
|
+
* Emitted in sorted key order, because `Record` iteration order is insertion order and data round-tripped
|
|
29
|
+
* through Firestore carries no insertion-order guarantee.
|
|
30
|
+
*
|
|
31
|
+
* @param data - The stored extension data.
|
|
32
|
+
* @returns The extra properties. Empty when nothing survives sanitization.
|
|
33
|
+
*
|
|
34
|
+
* @__NO_SIDE_EFFECTS__
|
|
35
|
+
*/
|
|
36
|
+
export declare function calendarExtensionDataToICalendarExtraProperties(data: Maybe<CalendarExtensionData>): ICalendarExtraProperty[];
|
|
37
|
+
/**
|
|
38
|
+
* Configuration for {@link calendarToICalendar}.
|
|
39
|
+
*/
|
|
40
|
+
export interface CalendarToICalendarConfig {
|
|
41
|
+
/**
|
|
42
|
+
* The calendar's document id. Used as the UID prefix, so an event's UID stays stable across republishes.
|
|
43
|
+
*/
|
|
44
|
+
readonly calendarId: CalendarId;
|
|
45
|
+
/**
|
|
46
|
+
* How recurring events are emitted. Defaults to {@link DEFAULT_CALENDAR_ICS_RECURRENCE_MODE}.
|
|
47
|
+
*/
|
|
48
|
+
readonly recurrenceMode?: Maybe<CalendarIcsRecurrenceMode>;
|
|
49
|
+
/**
|
|
50
|
+
* The window expanded in `expand` mode. Defaults to the type config's expansion days around `now`.
|
|
51
|
+
*/
|
|
52
|
+
readonly expansionRange?: Maybe<DateRange>;
|
|
53
|
+
/**
|
|
54
|
+
* The UID factory. Built from {@link domain} when absent.
|
|
55
|
+
*/
|
|
56
|
+
readonly uidFactory?: Maybe<ICalendarUidFactory>;
|
|
57
|
+
/**
|
|
58
|
+
* The domain the UID factory is built around. REQUIRED when no {@link uidFactory} is supplied — the
|
|
59
|
+
* factory deliberately has no random fallback, since a UID that changes between publishes makes every
|
|
60
|
+
* client create a duplicate event rather than update the one it holds.
|
|
61
|
+
*/
|
|
62
|
+
readonly domain?: Maybe<string>;
|
|
63
|
+
/**
|
|
64
|
+
* The canonical URL the feed is published at. Emitted as SOURCE.
|
|
65
|
+
*/
|
|
66
|
+
readonly source?: Maybe<WebsiteUrl>;
|
|
67
|
+
/**
|
|
68
|
+
* Emitted as REFRESH-INTERVAL / X-PUBLISHED-TTL.
|
|
69
|
+
*/
|
|
70
|
+
readonly refreshInterval?: Maybe<Minutes>;
|
|
71
|
+
/**
|
|
72
|
+
* Whether a VTIMEZONE is derived for every zone the events reference. Defaults to true.
|
|
73
|
+
*
|
|
74
|
+
* Free to leave on: the deriver returns the calendar unchanged when no zoned value exists.
|
|
75
|
+
*/
|
|
76
|
+
readonly deriveTimezones?: Maybe<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* The reference instant for the default expansion window. Defaults to the current time.
|
|
79
|
+
*/
|
|
80
|
+
readonly now?: Maybe<Date>;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Builds the {@link CalendarToICalendarConfig} fragment a {@link CalendarTypeConfig} contributes.
|
|
84
|
+
*
|
|
85
|
+
* @param config - The calendar type config.
|
|
86
|
+
* @returns The ICS emission settings it carries.
|
|
87
|
+
*
|
|
88
|
+
* @__NO_SIDE_EFFECTS__
|
|
89
|
+
*/
|
|
90
|
+
export declare function calendarTypeConfigIcsConfig(config: CalendarTypeConfig): Pick<CalendarToICalendarConfig, 'recurrenceMode' | 'refreshInterval'>;
|
|
91
|
+
/**
|
|
92
|
+
* Returns the expansion window used by `expand` mode when the caller supplies none.
|
|
93
|
+
*
|
|
94
|
+
* @param config - The calendar type config carrying the window sizes.
|
|
95
|
+
* @param now - The reference instant.
|
|
96
|
+
* @returns The expansion range.
|
|
97
|
+
*
|
|
98
|
+
* @__NO_SIDE_EFFECTS__
|
|
99
|
+
*/
|
|
100
|
+
export declare function calendarTypeConfigIcsExpansionRange(config: CalendarTypeConfig, now: Date): DateRange;
|
|
101
|
+
/**
|
|
102
|
+
* The identity a single event item is rendered under.
|
|
103
|
+
*/
|
|
104
|
+
export interface CalendarEventItemToICalendarEventConfig {
|
|
105
|
+
/**
|
|
106
|
+
* The event's UID. Produced by the feed's UID factory, so a VEVENT rendered here and the same event in
|
|
107
|
+
* the published feed are the same event to a client rather than two.
|
|
108
|
+
*/
|
|
109
|
+
readonly uid: ICalendarUid;
|
|
110
|
+
/**
|
|
111
|
+
* The timezone the event's wall clock is anchored to. Normally `calendarEventItemTimezone(item, calendar.tz)`.
|
|
112
|
+
*/
|
|
113
|
+
readonly timezone: TimezoneString;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Renders a ONE-OFF event item as a VEVENT.
|
|
117
|
+
*
|
|
118
|
+
* Goes through `iCalendarEventForCalendarDate()`, which already handles the all-day `VALUE=DATE` path and
|
|
119
|
+
* RFC 5545 3.8.2.2's exclusive DTEND. A consequence worth knowing: a TIMED one-off emits UTC instants,
|
|
120
|
+
* because that factory has no zoned path. Deliberate -- it needs no VTIMEZONE and reads identically in
|
|
121
|
+
* every client.
|
|
122
|
+
*
|
|
123
|
+
* @param item - The stored event item.
|
|
124
|
+
* @param config - The UID and timezone to render it under.
|
|
125
|
+
* @returns The event.
|
|
126
|
+
*
|
|
127
|
+
* @__NO_SIDE_EFFECTS__
|
|
128
|
+
*/
|
|
129
|
+
export declare function calendarEventItemToICalendarEvent(item: CalendarEventItem, config: CalendarEventItemToICalendarEventConfig): ICalendarEvent;
|
|
130
|
+
/**
|
|
131
|
+
* Renders a RECURRING event item as a single rule-bearing VEVENT.
|
|
132
|
+
*
|
|
133
|
+
* Gets a ZONED DTSTART when its timezone is not UTC. This is where it matters: a `FREQ=WEEKLY;BYDAY=MO`
|
|
134
|
+
* rule anchored to a UTC instant drifts across a DST boundary. It carries `duration`, never `end` -- the
|
|
135
|
+
* serializer prefers DTEND when both are set, and a per-series DTEND is wrong.
|
|
136
|
+
*
|
|
137
|
+
* @param item - The stored recurring event item.
|
|
138
|
+
* @param config - The UID and timezone to render it under.
|
|
139
|
+
* @returns The event.
|
|
140
|
+
*
|
|
141
|
+
* @__NO_SIDE_EFFECTS__
|
|
142
|
+
*/
|
|
143
|
+
export declare function calendarRecurringEventItemToICalendarEvent(item: CalendarRecurringEventItem, config: CalendarEventItemToICalendarEventConfig): ICalendarEvent;
|
|
144
|
+
/**
|
|
145
|
+
* Converts a {@link Calendar} into the format-agnostic {@link ICalendar} model.
|
|
146
|
+
*
|
|
147
|
+
* - A ONE-OFF event goes through `iCalendarEventForCalendarDate()`, which already handles the all-day
|
|
148
|
+
* `VALUE=DATE` path and RFC 5545 3.8.2.2's exclusive DTEND. A consequence worth knowing: a TIMED one-off
|
|
149
|
+
* emits UTC instants, because that factory has no zoned path. Deliberate — it needs no VTIMEZONE and reads
|
|
150
|
+
* identically in every client.
|
|
151
|
+
* - A RECURRING event gets a ZONED DTSTART when its timezone is not UTC. This is where it matters: a
|
|
152
|
+
* `FREQ=WEEKLY;BYDAY=MO` rule anchored to a UTC instant drifts across a DST boundary. It carries
|
|
153
|
+
* `duration`, never `end` — the serializer prefers DTEND when both are set, and a per-series DTEND is wrong.
|
|
154
|
+
* - `recurrenceMode: 'expand'` instead emits one VEVENT per occurrence, each carrying a RECURRENCE-ID.
|
|
155
|
+
*
|
|
156
|
+
* @param calendar - The calendar to render.
|
|
157
|
+
* @param config - The UID source, recurrence mode, and feed metadata.
|
|
158
|
+
* @returns The iCalendar model.
|
|
159
|
+
* @throws {Error} If neither a uidFactory nor a domain is supplied.
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```ts
|
|
163
|
+
* const iCalendar = calendarToICalendar(calendar, { calendarId, domain: 'example.com' });
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
export declare function calendarToICalendar(calendar: Calendar, config: CalendarToICalendarConfig): ICalendar;
|
|
167
|
+
/**
|
|
168
|
+
* Converts a single expanded occurrence into a discrete VEVENT.
|
|
169
|
+
*
|
|
170
|
+
* Used by `expand` mode, and by any caller that wants one event per occurrence rather than a rule.
|
|
171
|
+
*
|
|
172
|
+
* @param occurrence - The occurrence to render.
|
|
173
|
+
* @param uidFactory - The UID factory, fed the occurrence's stable key.
|
|
174
|
+
* @returns The event.
|
|
175
|
+
*
|
|
176
|
+
* @__NO_SIDE_EFFECTS__
|
|
177
|
+
*/
|
|
178
|
+
export declare function calendarEventOccurrenceToICalendarEvent(occurrence: CalendarEventOccurrence, uidFactory: ICalendarUidFactory): ICalendarEvent;
|
|
179
|
+
/**
|
|
180
|
+
* Renders a {@link Calendar} into a complete, folded, CRLF-terminated ICS document.
|
|
181
|
+
*
|
|
182
|
+
* @param calendar - The calendar to render.
|
|
183
|
+
* @param config - The iCalendar config plus serialization settings (notably `now`, the DTSTAMP source).
|
|
184
|
+
* @returns The ICS document.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* const ics = calendarToIcsString(calendar, { calendarId, domain: 'example.com', now: calendar.uat });
|
|
189
|
+
* ```
|
|
190
|
+
*/
|
|
191
|
+
export declare function calendarToIcsString(calendar: Calendar, config: CalendarToICalendarConfig & ICalendarSerializeConfig): ICalendarIcsString;
|
|
192
|
+
/**
|
|
193
|
+
* The timezone treated as "no zone needed" when deciding whether a recurring DTSTART must be zoned.
|
|
194
|
+
*/
|
|
195
|
+
export declare const CALENDAR_ICS_DEFAULT_TIMEZONE = "UTC";
|
|
196
|
+
/**
|
|
197
|
+
* The iTIP method an invite carries when its caller names none.
|
|
198
|
+
*
|
|
199
|
+
* REQUEST rather than PUBLISH deliberately: REQUEST is the only method that can ever carry a real RSVP, so
|
|
200
|
+
* starting here makes adding accept/decline later a parameter change rather than a redesign. The cost is
|
|
201
|
+
* that REQUEST needs an ATTENDEE matching the recipient's own address, which is what forces a sending
|
|
202
|
+
* service to emit one request per recipient rather than one batched request.
|
|
203
|
+
*/
|
|
204
|
+
export declare const DEFAULT_CALENDAR_INVITE_METHOD: ICalendarMethod;
|
|
205
|
+
/**
|
|
206
|
+
* The PARTSTAT an invite attendee is given when its caller names none.
|
|
207
|
+
*
|
|
208
|
+
* Pre-accepting suppresses the RSVP prompt in most clients while still getting auto-add and a proper inline
|
|
209
|
+
* invitation card. A strong hint rather than a guarantee -- some clients render RSVP buttons regardless.
|
|
210
|
+
*/
|
|
211
|
+
export declare const DEFAULT_CALENDAR_INVITE_ATTENDEE_PARTICIPATION_STATUS: ICalendarParticipationStatus;
|
|
212
|
+
/**
|
|
213
|
+
* The ROLE an invite attendee is given when its caller names none.
|
|
214
|
+
*/
|
|
215
|
+
export declare const DEFAULT_CALENDAR_INVITE_ATTENDEE_ROLE: ICalendarAttendeeRole;
|
|
216
|
+
/**
|
|
217
|
+
* Configuration for {@link calendarEventItemToInviteICalendar} / {@link calendarEventItemToInviteIcsString}.
|
|
218
|
+
*/
|
|
219
|
+
export interface CalendarEventItemToInviteConfig {
|
|
220
|
+
/**
|
|
221
|
+
* The event to invite to. A recurring item (one carrying `rr`) is emitted as a single rule-bearing VEVENT,
|
|
222
|
+
* exactly as the feed emits it in `rule` mode.
|
|
223
|
+
*/
|
|
224
|
+
readonly item: CalendarEventItem | CalendarRecurringEventItem;
|
|
225
|
+
/**
|
|
226
|
+
* The calendar's document id. The UID prefix, and it MUST be the same id the feed publishes the event
|
|
227
|
+
* under -- see {@link uidFactory}.
|
|
228
|
+
*/
|
|
229
|
+
readonly calendarId: CalendarId;
|
|
230
|
+
/**
|
|
231
|
+
* The iTIP method. Defaults to {@link DEFAULT_CALENDAR_INVITE_METHOD}.
|
|
232
|
+
*
|
|
233
|
+
* CANCEL emits a CANCELLED status and, absent an explicit {@link sequence}, bumps the sequence -- see
|
|
234
|
+
* {@link sequence}.
|
|
235
|
+
*/
|
|
236
|
+
readonly method?: Maybe<ICalendarMethod>;
|
|
237
|
+
/**
|
|
238
|
+
* Who the invite is from.
|
|
239
|
+
*
|
|
240
|
+
* Point this at a mailbox that ARCHIVES OR DISCARDS: we are the ORGANIZER, so inbound `METHOD:REPLY`
|
|
241
|
+
* mail is possible even when every attendee is sent `RSVP=FALSE`, and it will arrive at this address.
|
|
242
|
+
*/
|
|
243
|
+
readonly organizer: ICalendarOrganizer;
|
|
244
|
+
/**
|
|
245
|
+
* Who the invite is to. Defaults are applied per attendee -- see
|
|
246
|
+
* {@link DEFAULT_CALENDAR_INVITE_ATTENDEE_PARTICIPATION_STATUS} and
|
|
247
|
+
* {@link DEFAULT_CALENDAR_INVITE_ATTENDEE_ROLE}.
|
|
248
|
+
*
|
|
249
|
+
* A REQUEST is only rendered inline by a client that finds ITS OWN address here, which is why an invite
|
|
250
|
+
* is built per recipient rather than once per notification.
|
|
251
|
+
*/
|
|
252
|
+
readonly attendees: ArrayOrValue<ICalendarAttendee>;
|
|
253
|
+
/**
|
|
254
|
+
* SEQUENCE override.
|
|
255
|
+
*
|
|
256
|
+
* Defaults to the item's own revision counter (`q`), which the Calendar model already bumps on every
|
|
257
|
+
* change a subscriber can observe -- so an updated event re-sent under the same UID outranks the copy the
|
|
258
|
+
* client holds. Load-bearing: an update with a stale or absent SEQUENCE is SILENTLY IGNORED.
|
|
259
|
+
*
|
|
260
|
+
* For CANCEL the default is `q + 1` instead, so a cancellation always outranks the last REQUEST sent for
|
|
261
|
+
* the same revision. Pass an explicit value when the caller owns its own sequence source.
|
|
262
|
+
*/
|
|
263
|
+
readonly sequence?: Maybe<number>;
|
|
264
|
+
/**
|
|
265
|
+
* The UID factory.
|
|
266
|
+
*
|
|
267
|
+
* MUST be the FEED's factory (or an identical one built from the feed's {@link domain}). A second UID
|
|
268
|
+
* scheme means a recipient who is both subscribed to the feed and holding the invite sees the event
|
|
269
|
+
* twice, and no update or cancellation reaches the invited copy.
|
|
270
|
+
*/
|
|
271
|
+
readonly uidFactory?: Maybe<ICalendarUidFactory>;
|
|
272
|
+
/**
|
|
273
|
+
* The domain the UID factory is built around. REQUIRED when no {@link uidFactory} is supplied.
|
|
274
|
+
*/
|
|
275
|
+
readonly domain?: Maybe<string>;
|
|
276
|
+
/**
|
|
277
|
+
* The timezone the event's wall clock is anchored to. Normally the parent calendar's `tz`. Defaults to
|
|
278
|
+
* {@link CALENDAR_ICS_DEFAULT_TIMEZONE}.
|
|
279
|
+
*/
|
|
280
|
+
readonly timezone?: Maybe<TimezoneString>;
|
|
281
|
+
/**
|
|
282
|
+
* Optional display name for the enclosing VCALENDAR.
|
|
283
|
+
*/
|
|
284
|
+
readonly name?: Maybe<string>;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Builds the single-event iTIP {@link ICalendar} an emailed invite carries.
|
|
288
|
+
*
|
|
289
|
+
* The counterpart to {@link calendarToICalendar}: same model, same UID scheme, one event, and a METHOD.
|
|
290
|
+
* The METHOD is what makes it an iTIP message (RFC 5546) rather than a feed -- which is right for an
|
|
291
|
+
* emailed attachment and wrong for a subscription, hence the two entry points.
|
|
292
|
+
*
|
|
293
|
+
* @param config - The event, the parties, and the UID source.
|
|
294
|
+
* @returns The iCalendar model.
|
|
295
|
+
* @throws {Error} If neither a uidFactory nor a domain is supplied.
|
|
296
|
+
*
|
|
297
|
+
* @__NO_SIDE_EFFECTS__
|
|
298
|
+
*/
|
|
299
|
+
export declare function calendarEventItemToInviteICalendar(config: CalendarEventItemToInviteConfig): ICalendar;
|
|
300
|
+
/**
|
|
301
|
+
* Renders the emailed iTIP invite for a single event as a complete, folded, CRLF-terminated ICS document.
|
|
302
|
+
*
|
|
303
|
+
* The output is used directly as the calendar MIME part's data. The part must ALSO be typed
|
|
304
|
+
* `text/calendar; method=<method>; charset=utf-8` (see `iCalendarITipContentType()`) -- a client decides
|
|
305
|
+
* whether to auto-process from the part type, and the same bytes typed as an octet stream are a paperclip.
|
|
306
|
+
*
|
|
307
|
+
* @param config - The event, the parties, the UID source, and the DTSTAMP source.
|
|
308
|
+
* @returns The ICS document.
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```ts
|
|
312
|
+
* const ics = calendarEventItemToInviteIcsString({
|
|
313
|
+
* item,
|
|
314
|
+
* calendarId,
|
|
315
|
+
* domain: 'example.com',
|
|
316
|
+
* organizer: { address: 'calendar@example.com', name: 'Example' },
|
|
317
|
+
* attendees: { address: recipientEmail },
|
|
318
|
+
* now: item.uat
|
|
319
|
+
* });
|
|
320
|
+
* ```
|
|
321
|
+
*/
|
|
322
|
+
export declare function calendarEventItemToInviteIcsString(config: CalendarEventItemToInviteConfig & ICalendarSerializeConfig): ICalendarIcsString;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { type FlatFirestoreModelKey, inferKeyFromTwoWayFlatFirestoreModelKey, twoWayFlatFirestoreModelKey, type FirestoreModelId, type FirestoreModelKey } from '../../common';
|
|
2
|
+
/**
|
|
3
|
+
* @module calendar.id
|
|
4
|
+
*
|
|
5
|
+
* Identity types and id-generation patterns for the Calendar model.
|
|
6
|
+
*
|
|
7
|
+
* A Calendar that belongs to another model uses that model's key, two-way flat encoded, as its own document
|
|
8
|
+
* id — the same mechanism {@link StorageFileGroupId} uses. The profile "pr/abc123" therefore owns the
|
|
9
|
+
* calendar "cal/pr_abc123", loadable directly with no query and no lookup field.
|
|
10
|
+
*
|
|
11
|
+
* There is deliberately NO `modelKey` field on the Calendar itself: the id IS the association, and a
|
|
12
|
+
* queryable duplicate of it would be a second source of truth for the same fact.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Firestore document id for a Calendar.
|
|
16
|
+
*
|
|
17
|
+
* For a Calendar that belongs to a model this is a {@link FlatFirestoreModelKey}; for a standalone
|
|
18
|
+
* calendar it is an arbitrary id.
|
|
19
|
+
*/
|
|
20
|
+
export type CalendarId = FirestoreModelId;
|
|
21
|
+
/**
|
|
22
|
+
* Full Firestore document key (collection path + id) for a Calendar.
|
|
23
|
+
*/
|
|
24
|
+
export type CalendarKey = FirestoreModelKey;
|
|
25
|
+
/**
|
|
26
|
+
* Identifier for a single event embedded in a Calendar. Unique within its own calendar only.
|
|
27
|
+
*
|
|
28
|
+
* @semanticType
|
|
29
|
+
* @semanticTopic identifier
|
|
30
|
+
* @semanticTopic string
|
|
31
|
+
* @semanticTopic dereekb-firebase:calendar
|
|
32
|
+
*/
|
|
33
|
+
export type CalendarEventId = string;
|
|
34
|
+
/**
|
|
35
|
+
* Identifier for a single expanded OCCURRENCE of a calendar event.
|
|
36
|
+
*
|
|
37
|
+
* A one-off event's occurrence key is its {@link CalendarEventId}. A recurring event's occurrence key is
|
|
38
|
+
* its id plus the occurrence's unix seconds, which is what keeps a published VEVENT's UID stable across
|
|
39
|
+
* republishes of the same series.
|
|
40
|
+
*
|
|
41
|
+
* @semanticType
|
|
42
|
+
* @semanticTopic identifier
|
|
43
|
+
* @semanticTopic string
|
|
44
|
+
* @semanticTopic dereekb-firebase:calendar
|
|
45
|
+
*/
|
|
46
|
+
export type CalendarOccurrenceKey = string;
|
|
47
|
+
/**
|
|
48
|
+
* Separates a recurring event's id from its occurrence's unix seconds within a {@link CalendarOccurrenceKey}.
|
|
49
|
+
*/
|
|
50
|
+
export declare const CALENDAR_OCCURRENCE_KEY_SEPARATOR = "_";
|
|
51
|
+
/**
|
|
52
|
+
* Arbitrary string describing the kind of calendar a document is, driving its retention policy and its ICS
|
|
53
|
+
* emission config through the app's {@link CalendarTypeConfig} registry.
|
|
54
|
+
*
|
|
55
|
+
* Open by design, exactly like {@link StorageFilePurpose}: a downstream app registers its own types without
|
|
56
|
+
* a library change.
|
|
57
|
+
*
|
|
58
|
+
* @semanticType
|
|
59
|
+
* @semanticTopic identifier
|
|
60
|
+
* @semanticTopic string
|
|
61
|
+
* @semanticTopic dereekb-firebase:calendar
|
|
62
|
+
*/
|
|
63
|
+
export type CalendarType = string;
|
|
64
|
+
/**
|
|
65
|
+
* Arbitrary extension data attached to a Calendar or one of its events.
|
|
66
|
+
*
|
|
67
|
+
* THIS IS AN ICS EXTENSION LEVER, not general-purpose storage. Every entry is emitted as an "X-" property
|
|
68
|
+
* on the corresponding VCALENDAR/VEVENT, so values are STRINGS ONLY — an ICS property value simply is text.
|
|
69
|
+
* Anything structured is JSON-stringified into a single key by the caller.
|
|
70
|
+
*
|
|
71
|
+
* Keys are stored WITHOUT the "X-" prefix and are prefixed at emit time, which is what makes it impossible
|
|
72
|
+
* for a stored key to shadow a standard property like SUMMARY.
|
|
73
|
+
*/
|
|
74
|
+
export type CalendarExtensionData = Readonly<Record<string, string>>;
|
|
75
|
+
/**
|
|
76
|
+
* The status of a calendar event, mirroring RFC 5545 3.8.1.11.
|
|
77
|
+
*
|
|
78
|
+
* CANCELLED is the only way a published feed communicates a deletion to a client that already holds the
|
|
79
|
+
* event, so removing an event tombstones it as CANCELLED rather than splicing it out.
|
|
80
|
+
*/
|
|
81
|
+
export declare enum CalendarEventStatus {
|
|
82
|
+
TENTATIVE = "TENTATIVE",
|
|
83
|
+
CONFIRMED = "CONFIRMED",
|
|
84
|
+
CANCELLED = "CANCELLED"
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Encodes a {@link FirestoreModelKey} into the {@link CalendarId} of the Calendar that belongs to it.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* const calendarId = calendarIdForModel('pr/abc123'); // 'pr_abc123'
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export declare const calendarIdForModel: typeof twoWayFlatFirestoreModelKey;
|
|
95
|
+
/**
|
|
96
|
+
* Decodes a {@link CalendarId} back into the {@link FirestoreModelKey} of the model it belongs to.
|
|
97
|
+
*
|
|
98
|
+
* Returns a meaningless value for a standalone calendar with a random id, exactly as
|
|
99
|
+
* {@link inferStorageFileGroupRelatedModelKey} does.
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* ```ts
|
|
103
|
+
* const modelKey = inferCalendarRelatedModelKey('pr_abc123'); // 'pr/abc123'
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
export declare const inferCalendarRelatedModelKey: typeof inferKeyFromTwoWayFlatFirestoreModelKey;
|
|
107
|
+
/**
|
|
108
|
+
* A Calendar id that encodes the key of the model it belongs to.
|
|
109
|
+
*/
|
|
110
|
+
export type CalendarModelId = FlatFirestoreModelKey;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type SlashPath, type SlashPathFolder } from '@dereekb/util';
|
|
2
|
+
import { type StorageFileId, type StorageFileMetadata, type StorageFilePurpose } from '../storagefile/storagefile.id';
|
|
3
|
+
import { type StorageFileProcessingSubtask, type StorageFileProcessingSubtaskMetadata } from '../storagefile/storagefile.task';
|
|
4
|
+
import { type CalendarId } from './calendar.id';
|
|
5
|
+
/**
|
|
6
|
+
* @module calendar.processing
|
|
7
|
+
*
|
|
8
|
+
* The StorageFile purpose that publishes a Calendar's ".ics", mirroring `storagefile.group.processing.ts`.
|
|
9
|
+
*
|
|
10
|
+
* DIVERGENCE FROM THE ZIP FLOW, stated explicitly: a StorageFileGroup's zip StorageFile uses
|
|
11
|
+
* {@link StorageFileCreationType.FOR_STORAGE_FILE_GROUP}, which derives a DETERMINISTIC document id from its
|
|
12
|
+
* parent group. A Calendar is not a StorageFileGroup, so its ICS StorageFile is `DIRECTLY_CREATED` and its id
|
|
13
|
+
* is carried on `Calendar.isf`. That is the `zsf` half of the pattern without the deterministic-key half.
|
|
14
|
+
*
|
|
15
|
+
* BECAUSE the id is non-deterministic, the ICS path is keyed by the STORAGE FILE's id, not the calendar's.
|
|
16
|
+
* A calendar-keyed path would let two StorageFiles resolve to the same object: when the previous ICS
|
|
17
|
+
* StorageFile is QUEUED_FOR_DELETE but not yet swept, the sync creates a replacement, and the sweep's
|
|
18
|
+
* `storageService.file(oldStorageFile).delete()` would then delete the REPLACEMENT's content while its own
|
|
19
|
+
* document still read SUCCESS — a hole `flagStaleCalendarsForSync()` cannot heal, since it compares `sat`
|
|
20
|
+
* against `uat` and `sat` is newer. A StorageFile-keyed path makes that collision unrepresentable, and has
|
|
21
|
+
* the side benefit that the published object is not guessable from the owner's uid.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* {@link StorageFilePurpose} identifier for a Calendar's published ICS file.
|
|
25
|
+
*/
|
|
26
|
+
export declare const CALENDAR_ICS_STORAGE_FILE_PURPOSE: StorageFilePurpose;
|
|
27
|
+
/**
|
|
28
|
+
* Subtask checkpoint identifier for the ICS generation step of Calendar processing.
|
|
29
|
+
*/
|
|
30
|
+
export declare const CALENDAR_ICS_STORAGE_FILE_PURPOSE_GENERATE_ICS_SUBTASK: StorageFileProcessingSubtask;
|
|
31
|
+
/**
|
|
32
|
+
* Type alias for the ICS generation subtask checkpoint string.
|
|
33
|
+
*/
|
|
34
|
+
export type CalendarIcsStorageFileProcessingSubtask = typeof CALENDAR_ICS_STORAGE_FILE_PURPOSE_GENERATE_ICS_SUBTASK;
|
|
35
|
+
/**
|
|
36
|
+
* Metadata type for the ICS generation subtask.
|
|
37
|
+
*/
|
|
38
|
+
export type CalendarIcsStorageFileProcessingSubtaskMetadata = StorageFileProcessingSubtaskMetadata;
|
|
39
|
+
/**
|
|
40
|
+
* Metadata stored on the ICS StorageFile, linking it back to the Calendar it publishes.
|
|
41
|
+
*
|
|
42
|
+
* This is the ONLY link the processor has: the StorageFile is directly created, so its id carries no
|
|
43
|
+
* information about its calendar.
|
|
44
|
+
*/
|
|
45
|
+
export interface CalendarIcsStorageFileMetadata extends StorageFileMetadata {
|
|
46
|
+
/**
|
|
47
|
+
* Calendar id. Id of the Calendar this ICS file was generated for.
|
|
48
|
+
*/
|
|
49
|
+
readonly cal: CalendarId;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Root folder in Firebase Storage where all Calendar-generated files are stored.
|
|
53
|
+
*
|
|
54
|
+
* Flat by design: a Calendar has exactly one derived artifact, so there is nothing for a per-calendar
|
|
55
|
+
* subfolder to group. Contrast `storageFileGroupFolderPath()`, whose variadic sub-path earns its keep
|
|
56
|
+
* because a group holds many derived files.
|
|
57
|
+
*/
|
|
58
|
+
export declare const CALENDAR_ROOT_FOLDER_PATH: SlashPathFolder;
|
|
59
|
+
/**
|
|
60
|
+
* File extension of a Calendar's published ICS.
|
|
61
|
+
*/
|
|
62
|
+
export declare const CALENDAR_ICS_FILE_EXTENSION = ".ics";
|
|
63
|
+
/**
|
|
64
|
+
* Returns the storage path for a Calendar's published ICS file.
|
|
65
|
+
*
|
|
66
|
+
* Keyed by the ICS StorageFile's OWN id rather than the calendar's, so two StorageFiles can never resolve
|
|
67
|
+
* to the same object — see this module's header for the deletion collision that would otherwise exist.
|
|
68
|
+
*
|
|
69
|
+
* @param storageFileId - The id of the StorageFile that holds the published ICS.
|
|
70
|
+
* @returns The full path to the calendar's ICS file.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const icsPath = calendarIcsFileStoragePath('0mfR2xk8SqVe1Nb7'); // '/cal/0mfR2xk8SqVe1Nb7.ics'
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export declare function calendarIcsFileStoragePath(storageFileId: StorageFileId): SlashPath;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type FirestoreQueryConstraint } from '../../common/firestore/query/constraint';
|
|
2
|
+
import { type CalendarType } from './calendar.id';
|
|
3
|
+
/**
|
|
4
|
+
* Returns query constraints for Calendars flagged for sync (`s == true`).
|
|
5
|
+
*
|
|
6
|
+
* This is what the hourly sweep iterates.
|
|
7
|
+
*
|
|
8
|
+
* @returns Firestore query constraints for Calendars flagged for sync.
|
|
9
|
+
*
|
|
10
|
+
* @dbxModelFirebaseIndex
|
|
11
|
+
* @dbxModelFirebaseIndexModel Calendar
|
|
12
|
+
* @dbxModelFirebaseIndexScope COLLECTION
|
|
13
|
+
* @dbxModelFirebaseIndexCategory sweep
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const constraints = calendarsFlaggedForSyncQuery();
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function calendarsFlaggedForSyncQuery(): FirestoreQueryConstraint[];
|
|
21
|
+
/**
|
|
22
|
+
* Input for {@link calendarsDueForResyncQuery}.
|
|
23
|
+
*/
|
|
24
|
+
export interface CalendarsDueForResyncQueryInput {
|
|
25
|
+
/**
|
|
26
|
+
* The type to sweep. Each type carries its own resync interval, so the backstop sweeps one type at a time.
|
|
27
|
+
*/
|
|
28
|
+
readonly calendarType: CalendarType;
|
|
29
|
+
/**
|
|
30
|
+
* Calendars whose last successful publish predates this instant are due.
|
|
31
|
+
*/
|
|
32
|
+
readonly before: Date;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Returns query constraints for Calendars of a type whose last successful publish (`sat`) is older than the
|
|
36
|
+
* given instant.
|
|
37
|
+
*
|
|
38
|
+
* This is the self-healing backstop: it catches a Calendar whose sweep cleared `s` but whose ICS never
|
|
39
|
+
* finished publishing, and it keeps an `expand`-mode calendar from sliding off the end of its expansion
|
|
40
|
+
* window without any extra field or mechanism.
|
|
41
|
+
*
|
|
42
|
+
* NOTE: a Firestore inequality skips documents where the field is absent, so a Calendar that has NEVER
|
|
43
|
+
* published is not matched here. That case is already covered — it still carries `s == true` until its first
|
|
44
|
+
* successful sweep, and the ICS StorageFile's own retry / stuck-detection owns everything after that.
|
|
45
|
+
*
|
|
46
|
+
* @param input - The type to sweep and the staleness cutoff.
|
|
47
|
+
* @returns Firestore query constraints for Calendars due for a resync.
|
|
48
|
+
*
|
|
49
|
+
* @dbxModelFirebaseIndex
|
|
50
|
+
* @dbxModelFirebaseIndexModel Calendar
|
|
51
|
+
* @dbxModelFirebaseIndexScope COLLECTION
|
|
52
|
+
* @dbxModelFirebaseIndexCategory maintenance
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* const constraints = calendarsDueForResyncQuery({ calendarType: 'demo_profile', before: subDays(new Date(), 7) });
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function calendarsDueForResyncQuery(input: CalendarsDueForResyncQueryInput): FirestoreQueryConstraint[];
|
|
60
|
+
/**
|
|
61
|
+
* Returns query constraints for every Calendar of a given type.
|
|
62
|
+
*
|
|
63
|
+
* @param calendarType - The type to filter by.
|
|
64
|
+
* @returns Firestore query constraints for Calendars of the given type.
|
|
65
|
+
*
|
|
66
|
+
* @dbxModelFirebaseIndex
|
|
67
|
+
* @dbxModelFirebaseIndexModel Calendar
|
|
68
|
+
* @dbxModelFirebaseIndexScope COLLECTION
|
|
69
|
+
* @dbxModelFirebaseIndexCategory lookup
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```ts
|
|
73
|
+
* const constraints = calendarsForTypeQuery('demo_profile');
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export declare function calendarsForTypeQuery(calendarType: CalendarType): FirestoreQueryConstraint[];
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { type Maybe, type WebsiteUrl } from '@dereekb/util';
|
|
2
|
+
import { type FullDateCellScheduleRange } from '@dereekb/date';
|
|
3
|
+
import { type FirestoreModelKey } from '../../common';
|
|
4
|
+
import { type CalendarRecurringEventItem } from './calendar';
|
|
5
|
+
import { type CalendarEventId, type CalendarEventStatus, type CalendarExtensionData } from './calendar.id';
|
|
6
|
+
/**
|
|
7
|
+
* @module calendar.schedule
|
|
8
|
+
*
|
|
9
|
+
* Turns a `@dereekb/date` {@link FullDateCellScheduleRange} into a {@link CalendarRecurringEventItem}.
|
|
10
|
+
*
|
|
11
|
+
* This is the bridge that makes any model carrying a date-cell schedule publishable as calendar content. It
|
|
12
|
+
* is deliberately generic rather than living next to whichever model needs it first: nothing here knows what
|
|
13
|
+
* produced the schedule, only how a schedule becomes an event.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Configuration for {@link calendarRecurringEventItemForScheduleRange}.
|
|
17
|
+
*/
|
|
18
|
+
export interface CalendarRecurringEventItemForScheduleRangeConfig {
|
|
19
|
+
/**
|
|
20
|
+
* The schedule to publish.
|
|
21
|
+
*/
|
|
22
|
+
readonly range: FullDateCellScheduleRange;
|
|
23
|
+
/**
|
|
24
|
+
* Identifier for the generated event, unique within its calendar. Drives the published UID, so it must be
|
|
25
|
+
* STABLE across republishes of the same source model.
|
|
26
|
+
*/
|
|
27
|
+
readonly id: CalendarEventId;
|
|
28
|
+
/**
|
|
29
|
+
* Key of the model this schedule came from. Set this to make the event replaceable as a set via
|
|
30
|
+
* `replaceCalendarEventItemsForModelKey()`.
|
|
31
|
+
*/
|
|
32
|
+
readonly modelKey?: Maybe<FirestoreModelKey>;
|
|
33
|
+
/**
|
|
34
|
+
* Display name. Emitted as SUMMARY.
|
|
35
|
+
*/
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly description?: Maybe<string>;
|
|
38
|
+
readonly location?: Maybe<string>;
|
|
39
|
+
readonly url?: Maybe<WebsiteUrl>;
|
|
40
|
+
readonly status?: Maybe<CalendarEventStatus>;
|
|
41
|
+
readonly categories?: Maybe<string[]>;
|
|
42
|
+
readonly allDay?: Maybe<boolean>;
|
|
43
|
+
readonly extensionData?: Maybe<CalendarExtensionData>;
|
|
44
|
+
/**
|
|
45
|
+
* Emit `FREQ=DAILY` rather than `FREQ=WEEKLY` with all seven BYDAY tokens when every day is enabled.
|
|
46
|
+
* Defaults to true.
|
|
47
|
+
*/
|
|
48
|
+
readonly preferDailyFrequency?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* The creation/update instant. Defaults to the current time.
|
|
51
|
+
*/
|
|
52
|
+
readonly now?: Maybe<Date>;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Builds the recurring event that publishes a date-cell schedule.
|
|
56
|
+
*
|
|
57
|
+
* Returns undefined when the schedule yields no occurrences at all — an empty encoded week with no included
|
|
58
|
+
* days, or every day excluded. There is deliberately no "empty recurrence" value: an event whose rule
|
|
59
|
+
* produced nothing would still be retained and republished forever.
|
|
60
|
+
*
|
|
61
|
+
* EXCLUSIONS GO IN `rex`, NOT IN `rr`. The generator emits a self-contained rule with its own EXDATE line,
|
|
62
|
+
* but `calendarRecurringEventItemICalendarRecurrence()` (calendar.ics.ts) CONCATENATES the rule's exception
|
|
63
|
+
* dates with the ones from `rex` without deduplicating, so carrying them in both places emits every EXDATE
|
|
64
|
+
* twice. `rex` is the field the model documents for this, and keeping it there also holds `rr` to a single
|
|
65
|
+
* RRULE line plus an optional RDATE.
|
|
66
|
+
*
|
|
67
|
+
* @param config - The schedule, its identity, and the event's display metadata.
|
|
68
|
+
* @returns The recurring event, or undefined when the schedule has no occurrences.
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```ts
|
|
72
|
+
* const item = calendarRecurringEventItemForScheduleRange({
|
|
73
|
+
* range: fullDateCellScheduleRangeForJob(job),
|
|
74
|
+
* id: job.id,
|
|
75
|
+
* modelKey: jobDocument.key,
|
|
76
|
+
* name: job.n
|
|
77
|
+
* });
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* @__NO_SIDE_EFFECTS__
|
|
81
|
+
*/
|
|
82
|
+
export declare function calendarRecurringEventItemForScheduleRange(config: CalendarRecurringEventItemForScheduleRangeConfig): Maybe<CalendarRecurringEventItem>;
|