@happyvertical/smrt-events 0.53.4 → 0.53.5

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/README.md CHANGED
@@ -184,3 +184,80 @@ Owned asset helpers are available on both `Event` and `EventCollection` via
184
184
  | `@happyvertical/smrt-types` | Shared TypeScript types |
185
185
  | `@happyvertical/sql` | Database operations |
186
186
  | `@happyvertical/ai` | AI integration |
187
+
188
+ ### iCalendar source synchronization
189
+
190
+ `parseICalendarEvents(source, ics, { floatingTimeZone?, limits? })` validates
191
+ caller-supplied RFC 5545 text through the published `@happyvertical/icalendar`
192
+ facade. `expandICalendarEvents(source, ics, options)` adds a required inclusive
193
+ `rangeStart`/`rangeEnd` window. `syncICalendarSource({ db, source, ics, tenantId?,
194
+ parse?, expansion? })` persists those records and returns `created`, `updated`,
195
+ `unchanged`, and `retainedOnOmission: true`. Recurring feeds require `expansion`.
196
+ Parsing/expansion require no database or network; sync supports SQLite and
197
+ PostgreSQL with migrated Event/EventSeries schemas. Other dialects are not
198
+ currently supported ([EventSeries FK gap #3469](https://github.com/happyvertical/smrt/issues/3469)); runtime-schema adapters are rejected before mutation.
199
+
200
+ A tenant, source and UID identify a series. Its expanded occurrences use the
201
+ original RECURRENCE-ID (UTC ISO instant for timed events, calendar date for
202
+ all-day events), so a moved instance retains its ID. Non-recurring records use
203
+ UID without an occurrence suffix. Recurring masters live in series/provenance;
204
+ they are not duplicate Event rows alongside their first occurrence. RDATE is
205
+ normalized using each RDATE property's own timezone and unioned with RRULE,
206
+ duplicate instants collapse, and EXDATE slots persist as
207
+ cancelled events. Detached records override their original slot, including a
208
+ move beyond the requested window. Duplicate VEVENT identities reject the feed.
209
+
210
+ Daily, weekly, monthly and yearly RRULEs use the SDK's public recurrence engine.
211
+ The supported bounded subset is daily without BY filters; weekly BYDAY without
212
+ ordinals; monthly BYDAY or BYMONTHDAY (not both), with BYSETPOS supported only for BYDAY; and yearly
213
+ BYMONTH/BYMONTHDAY. Other BY combinations reject rather than risking unbounded
214
+ search inside the recurrence engine. COUNT, UNTIL and INTERVAL apply throughout.
215
+ Expansion rejects rather than truncates when it exceeds `limit` (default 1,000;
216
+ maximum 10,000) or `maxIterations` (default 10,000; maximum 100,000, including
217
+ slots before the window). Unsupported frequency, unknown recurrence properties,
218
+ multiple RRULEs, EXRULE, DURATION, RDATE PERIOD and RECURRENCE-ID RANGE reject.
219
+ SDK parser resource limits remain in force. Expand a practical window near the
220
+ series start; very old or dense feeds may need a deliberate higher scan budget.
221
+
222
+ IANA zones use the runtime's timezone database and the framework's wall-time
223
+ conversion: DST gaps move forward and overlaps choose the first instant. UTC
224
+ values stay UTC. Floating timed values require `parse.floatingTimeZone` (or the
225
+ same option directly on parse/expand); no host zone is inferred. All-day DATEs
226
+ use that policy or UTC, with exclusive DTEND and a default next-day end. A timed
227
+ event without DTEND has zero duration. Timed recurrence keeps the exact elapsed
228
+ duration of DTSTART to DTEND; all-day recurrence keeps calendar-day duration
229
+ across DST. Date-only recurrence remains date-only.
230
+
231
+ A higher SEQUENCE **or** a newer DTSTAMP accepts an update; equal/older versions
232
+ are unchanged. Missing SEQUENCE is zero and missing DTSTAMP is the oldest
233
+ stamp. A producer changing payload without either version marker is deliberately
234
+ ignored. Each series retains the authoritative master's SDK-serialized document
235
+ and version, including when a requested window contains no occurrences. Later
236
+ windows expand that retained document when the supplied master is stale or equal,
237
+ so unseen slots cannot be created from older payloads or recurrence rules. The
238
+ master document is written in the same transaction as occurrence updates. The
239
+ expansion requirement also applies to the retained document: a stale minimal
240
+ cancellation that resolves to a recurring master requires a window. Without one,
241
+ sync rejects and rolls back all writes; retry with bounded expansion.
242
+ A cancellation is an update, and a newer scheduled record restores it.
243
+ A minimal master cancellation (UID/STATUS/version without DTSTART) cancels known
244
+ series events and stores a series tombstone that blocks older resurrection.
245
+ No feed omission deletes events: outside-window records remain; omitted detached
246
+ exceptions stay authoritative until that detached identity reappears with a
247
+ newer version. A newer master removing EXDATE restores that generated slot.
248
+
249
+ Every sync resolves its database configuration once and performs all writes on
250
+ one transaction executor, including series creation. Failures propagate and
251
+ roll back; callers may retry the same feed. An existing SQLite/PostgreSQL
252
+ transaction is joined through an adapter savepoint, preserving caller rollback.
253
+ The caller must authorize tenant/source access and serialize concurrent syncs
254
+ for the same tenant/source; this API does not implement scheduling or locking.
255
+ An active tenant context supplies an omitted tenantId and rejects a conflicting
256
+ explicit tenantId, including a request for global rows. System contexts may
257
+ explicitly import a tenant or global source.
258
+
259
+ Source fetching (including conditional requests, unavailable-feed detection and
260
+ retry scheduling) remains caller-owned. Pass only a successfully fetched feed;
261
+ partial feeds are safe under omission retention. Sync performs no drafting or
262
+ publication. Event metadata records UID, occurrence ID, version markers,
263
+ RRULE/EXDATE/RDATE and whether the record is detached.
@@ -0,0 +1,53 @@
1
+ import { type ICalendarParseLimits } from '@happyvertical/icalendar';
2
+ /** A stable upstream identity for a calendar master or detached occurrence. */
3
+ export interface CalendarEventIdentity {
4
+ source: string;
5
+ uid: string;
6
+ recurrenceId?: string;
7
+ }
8
+ /** Parsed data SMRT needs before applying its persistence policy. */
9
+ export interface CalendarParseOptions {
10
+ floatingTimeZone?: string;
11
+ limits?: ICalendarParseLimits;
12
+ }
13
+ export interface CalendarEventInput {
14
+ identity: CalendarEventIdentity;
15
+ name: string;
16
+ description: string;
17
+ startDate: Date | null;
18
+ endDate: Date | null;
19
+ allDay: boolean;
20
+ timeZone: string;
21
+ status: 'scheduled' | 'cancelled';
22
+ sequence: number | null;
23
+ dtstamp: Date | null;
24
+ recurrence: string | null;
25
+ /** True for a detached record, false for a generated occurrence. */
26
+ detached?: boolean;
27
+ excludedRecurrenceIds: string[];
28
+ additionalRecurrenceIds: string[];
29
+ }
30
+ /** A malformed calendar event cannot be given a stable source identity. */
31
+ export declare class CalendarEventValidationError extends Error {
32
+ constructor(message: string);
33
+ }
34
+ /**
35
+ * Parse VEVENT records with their RFC 5545 source identities and recurrence
36
+ * exception data. This does not fetch a feed or persist anything.
37
+ */
38
+ export declare function parseICalendarEvents(source: string, ics: string, options?: CalendarParseOptions): CalendarEventInput[];
39
+ export interface CalendarExpansionOptions extends CalendarParseOptions {
40
+ rangeStart: Date;
41
+ rangeEnd: Date;
42
+ /** Hard cap: exceeding it rejects the feed, never returns a partial success. */
43
+ limit?: number;
44
+ /** Includes occurrences scanned before the requested window. */
45
+ maxIterations?: number;
46
+ }
47
+ /** Expand a caller-bounded window through the SDK's public recurrence engine.
48
+ * Range endpoints are inclusive. Detached records replace their original slot;
49
+ * EXDATE slots are returned as cancellations so previously persisted occurrences
50
+ * are cancelled as well. No master is returned alongside its first occurrence.
51
+ */
52
+ export declare function expandICalendarEvents(source: string, ics: string, options: CalendarExpansionOptions): CalendarEventInput[];
53
+ //# sourceMappingURL=icalendar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"icalendar.d.ts","sourceRoot":"","sources":["../../src/calendar/icalendar.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,oBAAoB,EAE1B,MAAM,0BAA0B,CAAC;AAOlC,+EAA+E;AAC/E,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,oBAAoB;IACnC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,GAAG,IAAI,CAAC;IACvB,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,WAAW,GAAG,WAAW,CAAC;IAClC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,uBAAuB,EAAE,MAAM,EAAE,CAAC;CACnC;AAED,2EAA2E;AAC3E,qBAAa,4BAA6B,SAAQ,KAAK;gBACzC,OAAO,EAAE,MAAM;CAI5B;AAsED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,oBAAyB,GACjC,kBAAkB,EAAE,CAkGtB;AAyBD,MAAM,WAAW,wBAAyB,SAAQ,oBAAoB;IACpE,UAAU,EAAE,IAAI,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,wBAAwB,GAChC,kBAAkB,EAAE,CAqOtB"}
@@ -0,0 +1,24 @@
1
+ import type { SmrtClassOptions } from '@happyvertical/smrt-core';
2
+ import { getDatabase } from '@happyvertical/sql';
3
+ import { type CalendarExpansionOptions, type CalendarParseOptions } from './icalendar';
4
+ export interface CalendarSourceSyncOptions extends Omit<SmrtClassOptions, 'db'> {
5
+ db: NonNullable<Parameters<typeof getDatabase>[0]>;
6
+ source: string;
7
+ ics: string;
8
+ tenantId?: string | null;
9
+ parse?: CalendarParseOptions;
10
+ expansion?: Omit<CalendarExpansionOptions, keyof CalendarParseOptions>;
11
+ }
12
+ export interface CalendarSourceSyncResult {
13
+ created: number;
14
+ updated: number;
15
+ unchanged: number;
16
+ /** Missing feed entries are deliberately retained; sync never deletes by omission. */
17
+ retainedOnOmission: true;
18
+ }
19
+ /**
20
+ * Persist one already-fetched calendar feed. The caller owns fetch, retries and
21
+ * authorization; every write runs through one required database transaction. Feed omission never deletes an existing event.
22
+ */
23
+ export declare function syncICalendarSource(options: CalendarSourceSyncOptions): Promise<CalendarSourceSyncResult>;
24
+ //# sourceMappingURL=sync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../src/calendar/sync.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAGjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAIjD,OAAO,EAEL,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EAG1B,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,yBACf,SAAQ,IAAI,CAAC,gBAAgB,EAAE,IAAI,CAAC;IACpC,EAAE,EAAE,WAAW,CAAC,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACnD,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,SAAS,CAAC,EAAE,IAAI,CAAC,wBAAwB,EAAE,MAAM,oBAAoB,CAAC,CAAC;CACxE;AAED,MAAM,WAAW,wBAAwB;IACvC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,sFAAsF;IACtF,kBAAkB,EAAE,IAAI,CAAC;CAC1B;AAsTD;;;GAGG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA4BnC"}
package/dist/index.d.ts CHANGED
@@ -6,6 +6,10 @@
6
6
  * @packageDocumentation
7
7
  */
8
8
  import './__smrt-register__.js';
9
+ export type { CalendarEventIdentity, CalendarEventInput, CalendarExpansionOptions, CalendarParseOptions, } from './calendar/icalendar';
10
+ export { CalendarEventValidationError, expandICalendarEvents, parseICalendarEvents, } from './calendar/icalendar';
11
+ export type { CalendarSourceSyncOptions, CalendarSourceSyncResult, } from './calendar/sync';
12
+ export { syncICalendarSource } from './calendar/sync';
9
13
  export { EventAssetCollection } from './collections/EventAssetCollection';
10
14
  export { EventCollection } from './collections/EventCollection';
11
15
  export { EventParticipantCollection } from './collections/EventParticipantCollection';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,wBAAwB,CAAC;AAEhC,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,YAAY,EACV,SAAS,EACT,uBAAuB,EACvB,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,YAAY,EACV,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,GACpB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,wBAAwB,CAAC;AAEhC,YAAY,EACV,qBAAqB,EACrB,kBAAkB,EAClB,wBAAwB,EACxB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,4BAA4B,EAC5B,qBAAqB,EACrB,oBAAoB,GACrB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EACV,yBAAyB,EACzB,wBAAwB,GACzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,0CAA0C,CAAC;AACtF,OAAO,EAAE,qBAAqB,EAAE,MAAM,qCAAqC,CAAC;AAE5E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,YAAY,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC7D,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,YAAY,EACV,SAAS,EACT,uBAAuB,EACvB,aAAa,GACd,MAAM,cAAc,CAAC;AAEtB,YAAY,EACV,YAAY,EACZ,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,wBAAwB,EACxB,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,wBAAwB,EACxB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAE3D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,uBAAuB,EACvB,gBAAgB,EAChB,cAAc,EACd,oBAAoB,EACpB,iBAAiB,EACjB,iBAAiB,EACjB,uBAAuB,EACvB,UAAU,EACV,eAAe,EACf,sBAAsB,EACtB,gBAAgB,EAChB,eAAe,EACf,mBAAmB,GACpB,MAAM,SAAS,CAAC"}