import { type FilterUniqueFunction, type Maybe, type Minutes, type NeedsSyncBoolean, type SortCompareFunction, type TimezoneString, type UnixDateTimeSecondsNumber, type WebsiteUrl } from '@dereekb/util'; import { type RRuleLines } from '@dereekb/date'; import { type GrantedReadRole, type GrantedUpdateRole } from '@dereekb/model'; import { AbstractFirestoreDocument, type CollectionReference, type FirestoreCollection, type FirestoreContext, type FirebaseAuthOwnershipKey, type FirestoreModelKey, type SavedToFirestoreIfTrue } from '../../common'; import { type CalendarEventId, type CalendarEventStatus, type CalendarExtensionData, type CalendarType } from './calendar.id'; import { type StorageFileId, type StorageFilePublicDownloadUrl } from '../storagefile'; /** * @module calendar * * Defines the Calendar Firestore model: a calendar and ALL of its events stored compactly in a single * document, published as an ".ics" file through the existing StorageFile processing machinery. * * **Why one document.** A downstream app reads the model directly and renders it, so what it shows is * always current — it never waits on, or re-parses, the published ICS. The cost is a 1 MiB ceiling and a * whole-array rewrite per edit, which is why growth is bounded by the {@link CalendarTypeConfig} retention * policy and why this shape suits publish-oriented calendars rather than high-churn shared ones. * * **Publishing.** Writing a Calendar flags it with `s` (needs sync). A scheduled sweep prunes it, creates or * re-flags the ICS StorageFile named by `isf`, and clears `s`. The StorageFile processing pipeline then * renders and uploads the file, inheriting its retry / stuck-detection / cleanup behaviour, and sets `sat` * on success. `StorageFileProcessingState.SUCCESS` therefore means "the published ICS matches this model". * * This is the same flow as StorageFileGroup → zip (`shouldRegenerate` flag → sweep → derived StorageFile → * subtask processor → upload), which is the reference implementation it mirrors. */ /** * Model identity for the Calendar collection (collection name: `calendar`, prefix: `cal`). */ export declare const calendarIdentity: import("../..").RootFirestoreModelIdentity<"calendar", "cal">; /** * A single non-recurring event embedded in a {@link Calendar}. * * Dates are stored as unix seconds rather than ISO strings, and the span is `{ startsAt, durationMinutes }` * rather than a start/end pair: both halve the stored bytes, and `{ startsAt, duration }` is already the * input shape of every `@dereekb/date` utility this model expands and emits through, so an end date would * mean converting back on every expansion and every ICS emit. * * @dbxModelSubObject */ export interface CalendarEventItem { /** * Identifier of the event, unique within its calendar. Stable across publishes. * * @dbxModelVariable eventId */ id: CalendarEventId; /** * Key of the model this event was generated from, when it was generated from one. * * A TARGETING HANDLE, not an identity: it is what lets a producer find and replace exactly the events it * owns (see `replaceCalendarEventItemsForModelKey()`) without tracking their generated ids. Several events * may share one key -- a schedule that emits a recurrence plus a few one-offs is one model, many events. * * Deliberately NOT the UID source. `calendarToICalendar()` feeds {@link id} to the UID factory, so this * field can be added to, or changed on, an already-published event without destabilising its UID. It is * also never emitted to the ICS, which is why it is exempt from the SEQUENCE bump -- see * `CALENDAR_EVENT_ITEM_CHANGE_IGNORED_FIELDS`. * * @dbxModelVariable modelKey */ m?: Maybe; /** * Instant the event starts at. * * For a recurring event this doubles as the recurrence's anchor — see {@link CalendarRecurringEventItem}. * * @dbxModelVariable startsAt */ sa: Date; /** * Duration of the event in minutes. * * @dbxModelVariable durationMinutes */ dur: Minutes; /** * True if the event occupies whole calendar days rather than an instant range. * * @dbxModelVariable allDay */ ad?: Maybe; /** * Timezone the event's wall clock is anchored to. Defaults to the calendar's timezone. * * For a recurring event this doubles as the recurrence's timezone. * * @dbxModelVariable timezone */ tz?: Maybe; /** * Display name of the event. Emitted as SUMMARY. * * @dbxModelVariable name */ n: string; /** * Longer description of the event. Emitted as DESCRIPTION. * * @dbxModelVariable description */ d?: Maybe; /** * Location of the event. Emitted as LOCATION. * * @dbxModelVariable location */ l?: Maybe; /** * Website for the event. Emitted as URL. * * @dbxModelVariable url */ u?: Maybe; /** * Status of the event. Emitted as STATUS. * * CANCELLED is a tombstone: it is how the feed tells a client that already holds the event that it was * removed. Retention is what eventually drops the tombstone. * * @dbxModelVariable status */ st?: Maybe; /** * Revision counter. Emitted as SEQUENCE. * * Subscribers compare it against the copy they hold to decide whether a same-UID event is newer, so it is * bumped on every semantic change to an already-published event. * * @dbxModelVariable sequence */ q?: Maybe; /** * Categories of the event. Emitted as CATEGORIES. * * @dbxModelVariable categories */ ca?: Maybe; /** * Extension data emitted as "X-" properties on this event's VEVENT. * * @dbxModelVariable extensionData */ x?: Maybe; /** * Created at date. * * @dbxModelVariable createdAt */ cat: Date; /** * Updated at date. * * @dbxModelVariable updatedAt */ uat: Date; } /** * A recurring event embedded in a {@link Calendar}. * * The recurrence fields are jointly required or jointly absent, so `extends` makes that a type-level * invariant rather than four `Maybe<>` fields plus a runtime guard. The two kinds live in two separate * arrays because their retention rules are structurally different (a one-off is pruned on its own end * instant, a recurrence on the series' end) and because the ICS mapper genuinely forks between them. * * There is deliberately NO `recurrenceStartsAt` / `recurrenceTimezone`: the base `sa` IS the recurrence's * start and the base `tz` IS its timezone, which makes the mapping to {@link ModelRecurrenceInfo} total and * lossless in both directions. * * @dbxModelSubObject */ export interface CalendarRecurringEventItem extends CalendarEventItem { /** * The recurrence rule, in the workspace's compact newline-joined storage form. * * NOTE: this KEEPS its "RRULE:" prefix and may carry EXDATE lines, so it cannot be handed to * {@link ICalendarRecurrence.rules} directly — see `iCalendarRecurrenceForRRuleLines()`. * * @dbxModelVariable recurrenceRule */ rr: RRuleLines; /** * Instant the final occurrence of the series ends at, when the series ends. * * @dbxModelVariable recurrenceEndsAt */ rea?: Maybe; /** * True if the series never ends. A forever recurrence is never pruned. * * @dbxModelVariable recurrenceForever */ rfe?: Maybe; /** * Occurrences excluded from the series, as unix seconds. * * Stored raw because no unix-seconds ARRAY snapshot field exists; `calendarEventItemExceptionDateSet()` * builds the DateSet the expansion wants. * * @dbxModelVariable recurrenceExceptionDates */ rex?: Maybe; } /** * Creates the comparison that orders calendar event items ascending by their start instant. * * The stored arrays are always in chronological order, which is what lets retention drop the oldest items * with a `slice` instead of a sort. * * A factory rather than a constant because it is used for both item types, and a * `SortCompareFunction` does not satisfy a `SortCompareFunction`. * * @returns The ascending-by-start comparison. * * @__NO_SIDE_EFFECTS__ */ export declare function calendarEventItemsSortFunction(): SortCompareFunction; /** * Creates the filter that keeps only the last entry carrying a given {@link CalendarEventId}. * * @returns The unique-by-id filter. * * @__NO_SIDE_EFFECTS__ */ export declare function calendarEventItemsFilterUniqueFunction(): FilterUniqueFunction; /** * The converter fields shared by {@link CalendarEventItem} and {@link CalendarRecurringEventItem}. * * Every optional field either uses an `optional*` factory or a `dontStoreIf`, so an absent field costs * nothing in the stored document. */ export declare const calendarEventItemFields: { id: import("../..").FirestoreModelFieldMapFunctionsConfig; m: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; sa: import("../..").FirestoreModelFieldMapFunctionsConfig; dur: import("../..").FirestoreModelFieldMapFunctionsConfig; ad: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; tz: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; n: import("../..").FirestoreModelFieldMapFunctionsConfig; d: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; l: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; u: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; st: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; q: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; ca: import("../..").FirestoreModelFieldMapFunctionsConfig, Maybe>; x: import("../..").FirestoreModelFieldMapFunctionsConfig>>, Maybe>>>; cat: import("../..").FirestoreModelFieldMapFunctionsConfig; uat: import("../..").FirestoreModelFieldMapFunctionsConfig; }; /** * Firestore sub-object converter for a {@link CalendarEventItem}. */ export declare const calendarEventItem: import("../..").FirestoreSubObjectFieldMapFunctionsConfig, any>>>; /** * Firestore sub-object converter for a {@link CalendarRecurringEventItem}. */ export declare const calendarRecurringEventItem: import("../..").FirestoreSubObjectFieldMapFunctionsConfig, any>>>; /** * A calendar and all of its events, stored in one document and published as an ".ics" file. * * A Calendar that belongs to another model uses that model's two-way flat key as its document id, so the * profile "pr/abc123" owns "cal/pr_abc123" — see {@link calendarIdForModel}. There is no `modelKey` field: * the id IS the association. * * `s` / `sat` / `isf` mirror {@link StorageFileGroup}'s `s` / `zat` / `zsf` exactly, and `o` drives * `resourceIsOwnedByAuthOwnershipKey()` in the security rules identically to `sf` / `sfg`. * * @dbxModel * @dbxModelRead owner */ export interface Calendar { /** * The kind of calendar this is, resolving its retention policy and ICS emission config. * * @dbxModelVariable calendarType */ t: CalendarType; /** * Display name of the calendar. Emitted as NAME/X-WR-CALNAME. * * @dbxModelVariable name */ n: string; /** * Description of the calendar. Emitted as DESCRIPTION/X-WR-CALDESC. * * @dbxModelVariable description */ d?: Maybe; /** * Default timezone of the calendar. Emitted as X-WR-TIMEZONE, and the fallback for an event with no `tz`. * * @dbxModelVariable timezone */ tz: TimezoneString; /** * CSS3 color name for the calendar. Emitted as COLOR. * * @dbxModelVariable color */ c?: Maybe; /** * Ownership key, if applicable. * * Drives read access in the security rules, and the `read` + `rotate` grants in the app's Calendar model * service — so this field, not the owning model's own role map, is the authoritative answer to "who may * revoke this calendar's published feed url". * * Absent means there is no owner to grant to, leaving the calendar reachable by a sys-admin only. * * @dbxModelVariable ownerKey */ o?: Maybe; /** * The calendar's one-off events, ascending by start instant and unique by id. * * @dbxModelVariable events */ e: CalendarEventItem[]; /** * The calendar's recurring events, ascending by anchor instant and unique by id. * * @dbxModelVariable recurringEvents */ r: CalendarRecurringEventItem[]; /** * Extension data emitted as "X-" properties on the calendar's VCALENDAR. * * @dbxModelVariable extensionData */ x?: Maybe; /** * Created at date. * * @dbxModelVariable createdAt */ cat: Date; /** * Updated at date. Moves on every content change. * * @dbxModelVariable updatedAt */ uat: Date; /** * True if this Calendar should be swept and its published ICS regenerated. * * Cleared inside the sync transaction, mirroring the `re` flag of the zip flow. * * @dbxModelVariable needsSync */ s?: Maybe; /** * The last date the published ICS was successfully uploaded. * * Set ONLY by the processor's success path. `s === false && sat < uat` therefore means "queued, not yet * published", which is exactly the state `flagStaleCalendarsForSync()` self-heals. * * @dbxModelVariable syncedAt */ sat?: Maybe; /** * The last date this calendar's published ICS link was rotated. * * The sole input to the rotation throttle: rotation revokes a url that subscribers have already stored, so * it is rate-limited rather than free. Both the server (which rejects an early rotation) and the client * (which disables the action until the window passes) read the window from this one field via * `calendarNextIcsRotateAt()`. * * Distinct from {@link Calendar.sat}, which moves on every successful publish — including the publish a * rotation triggers, and every hourly sweep after it. * * Absent means the link has never been rotated, which never throttles. * * @dbxModelVariable icsRotatedAt */ rat?: Maybe; /** * StorageFile that holds the published ICS for this calendar. * * @dbxModelVariable icsStorageFileId */ isf?: Maybe; /** * The permanent, anonymously-readable URL the published ICS is served from. * * Written ONLY by the processor's success path, alongside `isf` and `sat`, so it always names the object * whose bytes actually landed. Absent means "not yet published" — which is also the state a link rotation * leaves behind until the replacement ICS uploads. * * Stored rather than recomputed client-side because the host differs between the emulator and production, * and the object path is keyed by the ICS StorageFile's own id. * * TREAT AS A BEARER CREDENTIAL: anyone holding it reads the calendar until the link is rotated. * * @dbxModelVariable icsUrl */ iu?: Maybe; } /** * Permission roles for Calendar operations. * * - `read` is the owner's grant to render the model directly instead of the published .ics. * - `rotate` is the owner's capability to revoke the published feed url. A capability rather than a verb, * exactly as {@link StorageFileGroupRoles} models `regenerate`. * - `sync` is the publish-side role held by the scheduled sweep. * * `update` is representable and deliberately granted to nobody: every Calendar write has to carry `s` or the * publish sweep silently strands the feed, so generic writes stay server-only. */ export type CalendarRoles = GrantedReadRole | GrantedUpdateRole | 'rotate' | 'sync'; /** * Firestore document wrapper for a {@link Calendar}. * * Provides a convenience getter to infer the related model key from the calendar's own id. */ export declare class CalendarDocument extends AbstractFirestoreDocument { get modelIdentity(): import("../..").RootFirestoreModelIdentity<"calendar", "cal">; get calendarRelatedModelKey(): string; } /** * Snapshot converter for {@link Calendar} documents, including both embedded event arrays. */ export declare const calendarConverter: import("../..").SnapshotConverterFunctions, any>>>; /** * Returns the raw Firestore CollectionReference for the Calendar collection. * * @param context - The Firestore context to use. * @returns The CollectionReference for Calendar documents. */ export declare function calendarCollectionReference(context: FirestoreContext): CollectionReference; /** * Typed FirestoreCollection for {@link Calendar} documents. */ export type CalendarFirestoreCollection = FirestoreCollection; /** * Creates a fully configured {@link CalendarFirestoreCollection} with snapshot conversion and document factory. * * @param firestoreContext - The Firestore context to use. * @returns A configured CalendarFirestoreCollection. * * @example * ```ts * const collection = calendarFirestoreCollection(firestoreContext); * const doc = collection.documentAccessor().loadDocumentForId(calendarIdForModel(profileDocument.key)); * ``` */ export declare function calendarFirestoreCollection(firestoreContext: FirestoreContext): CalendarFirestoreCollection; /** * Abstract base providing access to the Calendar Firestore collection. * * Implement this in your app module to wire up the collection for dependency injection. * * @dbxModelGroup Calendar */ export declare abstract class CalendarFirestoreCollections { abstract readonly calendarCollection: CalendarFirestoreCollection; } /** * Union of all Calendar-related model identity types. */ export type CalendarTypes = typeof calendarIdentity;