Documentation
¶
Overview ¶
Package calendar is the durable calendar store behind the chassis `calendar` personality (CalDAV + ICS feeds) and the txco://calendar/* ops: accounts, per-account calendars, and per-calendar iCalendar objects. The object bytes live here, not in the blob CAS — the protocol semantics are mutable (PUT replaces, DELETE deletes) and the store must support real deletion.
Storage is dialect-aware (registry.Dialect, the seam auth, scheduled and imap use); the bundled backend is a SQLite file of its own. It is deliberately NOT a set of runtime tables: the dbcache watcher reloads the whole runtime mirror on any runtime-DB write.
Identity rules: an object is addressed by its resource name (the URL segment) and carries a UID unique within its calendar. A stack put addresses BY UID and keeps whatever resource name the object already has, so a client-created event and its later re-materializations are one object. The stored bytes are always a canonical iCalendar encoding; the ETag is their sha256; a put whose content differs only in DTSTAMP or SEQUENCE is a no-op, so an hourly re-materialization never churns etags. Deletes leave a tombstone (deleted_at) with a bumped modseq so a sync-collection REPORT can be added without a schema change; a later put of the same resource name resurrects the row.
Index ¶
- Constants
- Variables
- func Canonical(b []byte) ([]byte, error)
- func Decode(b []byte) (*ical.Calendar, error)
- func DefaultObjectName(uid string) string
- func DefaultUID(name, username string) string
- func ETagOf(ical []byte) string
- func Encode(cal *ical.Calendar) ([]byte, error)
- func FeedBytes(name, description string, objs []Object) ([]byte, error)
- func Master(cal *ical.Calendar) (master *ical.Component, overrides []*ical.Component, err error)
- func NormalizeUsername(u string) string
- func PolicyMode(cal *Calendar, acct *Account, verb string) string
- func Register(name string, c Constructor)
- func Render(ev Event, now time.Time) ([]byte, error)
- func SameContent(a, b []byte) bool
- func VTimezone(loc *time.Location, now time.Time) *ical.Component
- func ValidCalendarName(name string) bool
- func ValidMode(s string) bool
- func ValidObjectName(name string) bool
- func ValidatePolicy(raw json.RawMessage) error
- type Account
- type Calendar
- type Config
- type Constructor
- type Event
- type ListOpts
- type Object
- type PutOpts
- type PutResult
- type Recur
- type Store
- func (s *Store) CalendarByFeedHash(ctx context.Context, hash string) (Calendar, bool, error)
- func (s *Store) Close() error
- func (s *Store) DB() *sql.DB
- func (s *Store) DeleteObject(ctx context.Context, calID, name string) (string, bool, error)
- func (s *Store) EnsureCalendar(ctx context.Context, c Calendar) (Calendar, bool, error)
- func (s *Store) EnsureSchema(ctx context.Context) error
- func (s *Store) GetAccount(ctx context.Context, username string) (Account, bool, error)
- func (s *Store) GetCalendar(ctx context.Context, tenant, username, name string) (Calendar, bool, error)
- func (s *Store) GetCalendarByID(ctx context.Context, id string) (Calendar, bool, error)
- func (s *Store) GetObject(ctx context.Context, calID, name string) (Object, bool, error)
- func (s *Store) GetObjectByUID(ctx context.Context, calID, uid string) (Object, bool, error)
- func (s *Store) ListCalendars(ctx context.Context, tenant, username string) ([]Calendar, error)
- func (s *Store) ListObjects(ctx context.Context, calID string, opts ListOpts) ([]Object, error)
- func (s *Store) ObjectsInRange(ctx context.Context, calID string, start, end time.Time) ([]Object, error)
- func (s *Store) PutEvent(ctx context.Context, calID, username, name string, ev Event, now time.Time) (PutResult, error)
- func (s *Store) PutICal(ctx context.Context, calID, name string, ical []byte) (PutResult, error)
- func (s *Store) PutObject(ctx context.Context, calID string, obj Object, opts PutOpts) (PutResult, error)
- func (s *Store) RemoveCalendar(ctx context.Context, id string) (bool, error)
- func (s *Store) SetCalendarProps(ctx context.Context, id string, displayName, description, color *string, ...) error
- func (s *Store) SetClock(now func() time.Time)
- func (s *Store) SetFeedToken(ctx context.Context, id, hash string) error
- func (s *Store) UpsertAccount(ctx context.Context, tenant, username, status string, policy json.RawMessage) (created bool, err error)
Constants ¶
const ( VerbPut = "put" VerbDelete = "delete" VerbMkcalendar = "mkcalendar" VerbRemove = "remove" VerbProppatch = "proppatch" ModeDeny = "deny" ModeLocal = "local" ModeObserve = "observe" ModeStack = "stack" )
Policy vocabulary: per calendar, five verbs, each one of four modes. The chassis has no opinion about what a calendar holds; it has one about which client mutations a stack hears of, and when.
deny refused at the protocol layer (403), no round trip local protocol state only, no event observe commit, then tell the `_calendar` stack (fire-and-forget) stack ask the `_calendar` stack first; absent/false @calendar.res.ok ⇒ 403
Resolution: the calendar's own policy, then the account's default policy, then the chassis default — `put`/`delete` observe, `proppatch` local (display name / colour changes), `mkcalendar`/`remove` deny (calendars are provisioned by ops unless a product opens that).
const ( StatusActive = "active" StatusDisabled = "disabled" )
Account statuses.
const ProdID = "-//loremlabs//txco calendar//EN"
ProdID identifies chassis-rendered calendars.
Variables ¶
var ( // ErrUsernameTaken is returned by UpsertAccount when the username already // belongs to another tenant (usernames are globally unique: they are the // login identity). ErrUsernameTaken = errors.New("calendar: username belongs to another tenant") // ErrPrecondition is returned by PutObject when an If-Match / If-None-Match // condition fails. ErrPrecondition = errors.New("calendar: precondition failed") // ErrUIDConflict is returned by PutObject when the object's UID already // names a different live resource in the calendar (RFC 4791 §5.3.2.1). ErrUIDConflict = errors.New("calendar: uid already used by another resource") // ErrNotFound is returned when a calendar or object does not exist. ErrNotFound = errors.New("calendar: not found") )
var ErrInvalidEvent = errors.New("calendar: invalid event")
ErrInvalidEvent wraps a validation failure of the event or its bytes, so callers can answer "invalid argument" rather than "store failure".
var PolicyVerbs = []string{VerbPut, VerbDelete, VerbMkcalendar, VerbRemove, VerbProppatch}
PolicyVerbs lists the verbs a policy object may name.
Functions ¶
func DefaultObjectName ¶
DefaultObjectName derives a resource name from a UID's local part.
func DefaultUID ¶
DefaultUID is the deterministic UID for a materialized object: <name without .ics>.<local>@<domain> — stable across re-materializations, unique per persona. Stacks that spell a UID out (OnePony's projection) must derive the same value.
func Encode ¶
Encode is the canonical encoding: go-ical's encoder (sorted property names, children in order, 75-octet folding, CRLF).
func FeedBytes ¶
FeedBytes renders an ICS subscription feed: one VCALENDAR carrying every live object's components, VTIMEZONEs de-duplicated by TZID.
func Master ¶
Master returns the object's master component: the first VEVENT without a RECURRENCE-ID, else the first VTODO. Overrides (VEVENTs with a RECURRENCE-ID) are returned separately.
func NormalizeUsername ¶
NormalizeUsername lowercases and trims a login / op username.
func PolicyMode ¶
PolicyMode resolves one verb for a calendar (nil for a top-level MKCALENDAR, which resolves from the account alone).
func Register ¶
func Register(name string, c Constructor)
Register adds a backend constructor. Called from a backend package's init().
func Render ¶
Render encodes ev as a canonical VCALENDAR (one VEVENT, a VTIMEZONE when the event carries a TZID). now is the DTSTAMP. Deterministic apart from DTSTAMP and the VTIMEZONE's yearly window, which the store's no-op rule ignores or tolerates.
func SameContent ¶
SameContent reports whether two canonical encodings differ only in their DTSTAMP / SEQUENCE / LAST-MODIFIED lines — the re-materialization no-op rule. Folded continuation lines are joined before the comparison.
func VTimezone ¶
VTimezone builds a VTIMEZONE for loc from Go's tzdata by listing the zone's transitions as explicit STANDARD/DAYLIGHT observances (no RRULE): valid RFC 5545, correct across DST, and simple. The window is [Jan 1 of last year, Jan 1 three years out) anchored to now's year, so the component changes once a year rather than on every render.
func ValidCalendarName ¶
ValidCalendarName reports whether name is a calendar path segment: a URL segment of up to 128 chars (clients mint UUIDs; ops use short lowercase names). `feed` cannot collide — it lives at another depth.
func ValidObjectName ¶
ValidObjectName reports whether name is an object resource name (a URL segment; the conventional `.ics` suffix is not required — clients choose).
func ValidatePolicy ¶
func ValidatePolicy(raw json.RawMessage) error
ValidatePolicy checks a policy object: every key a known verb, every value a mode.
Types ¶
type Account ¶
type Account struct {
Tenant string
Username string
Status string
Policy json.RawMessage
CreatedAt time.Time
UpdatedAt time.Time
}
Account is a calendar_accounts row.
type Calendar ¶
type Calendar struct {
ID string
Tenant string
Username string
Name string
DisplayName string
Description string
Color string
SortOrder int
Timezone string
Policy json.RawMessage
FeedTokenHash string
SyncToken int64
CreatedAt time.Time
UpdatedAt time.Time
}
Calendar is a live calendar_calendars row.
type Config ¶
type Config struct {
// DBPath is the bundled SQLite backend's file path (--calendar-db-path).
DBPath string
}
Config carries backend-selecting options resolved from chassis config. The bundled "sqlite" backend uses DBPath. Adding a field here doesn't affect existing backends (same posture as scheduled.Config / vector.Config).
type Constructor ¶
Constructor builds a Store from resolved config. It is expected to open its backing DB and call Store.EnsureSchema before returning.
type Event ¶
type Event struct {
UID string `json:"uid,omitempty"`
Summary string `json:"summary,omitempty"`
Description string `json:"description,omitempty"`
Location string `json:"location,omitempty"`
Status string `json:"status,omitempty"`
URL string `json:"url,omitempty"`
Start string `json:"start,omitempty"`
End string `json:"end,omitempty"`
Duration string `json:"duration,omitempty"`
TZID string `json:"tzid,omitempty"`
RRule string `json:"rrule,omitempty"`
ExDate []string `json:"exdate,omitempty"`
// Facts the chassis fills in when parsing; ignored when rendering.
Component string `json:"component,omitempty"`
AllDay bool `json:"all_day"`
StartUTC string `json:"start_utc,omitempty"`
EndUTC string `json:"end_utc,omitempty"`
Recurs bool `json:"recurs"`
Recur *Recur `json:"recur,omitempty"`
Sequence int64 `json:"sequence"`
DTStamp string `json:"dtstamp,omitempty"`
}
Event is the chassis's structured form of one VEVENT: generic iCalendar vocabulary, no product words. On the way IN (txco://calendar/put WITH event, @calendar.res.event) the first block is read; on the way OUT (@calendar.event, txco://calendar/get) the facts are filled in as well.
Times: `start`/`end` are "YYYY-MM-DD" (all-day), RFC3339 ("…Z" or with an offset, stored as UTC), or a local "YYYY-MM-DDTHH:MM:SS" together with `tzid` (an IANA name). Floating times are refused. `duration` is an RFC 5545 duration ("PT30M"); give `end` or `duration`, not both; neither means PT1H (P1D for all-day). `rrule` is an RFC 5545 RECUR value ("FREQ=DAILY"), `exdate` entries take the same forms as `start`.
func EventFromJSON ¶
EventFromJSON reads an Event from its JSON form (the WITH event{} param or a stack's @calendar.res.event).
type Object ¶
type Object struct {
CalendarID string
Name string
UID string
ETag string
Component string
ICal []byte
Size int64
Summary string
DTStartUTC string // RFC3339 UTC of the first occurrence ("" when unknown)
DTEndUTC string
Recurs bool
Sequence int64
ModSeq int64
CreatedAt time.Time
UpdatedAt time.Time
Deleted bool
}
Object is a calendar_objects row.
type PutOpts ¶
type PutOpts struct {
// ByUID addresses the object by obj.UID: an existing live object with
// that UID is updated in place under its own resource name; obj.Name is
// used only when creating. This is the stack's mode.
ByUID bool
// IfMatch / IfNoneMatch are the client's conditional headers, unquoted
// ("*" for IfNoneMatch means create-only).
IfMatch string
IfNoneMatch string
}
PutOpts steers PutObject.
type PutResult ¶
type PutResult struct {
Name string
UID string
ETag string
Created bool
Noop bool
Sequence int64
ModSeq int64
}
PutResult reports what PutObject did.
type Recur ¶
type Recur struct {
Freq string `json:"freq"`
Interval int `json:"interval"`
ByDay []string `json:"byday"`
ByMonthDay []int `json:"bymonthday"`
ByMonth []int `json:"bymonth"`
ByHour []int `json:"byhour"`
ByMinute []int `json:"byminute"`
BySetPos []int `json:"bysetpos"`
Count int `json:"count"`
Until string `json:"until"`
}
Recur is the parsed RRULE — rrule-go's option set in JSON clothing so a sandboxed compute can read a rule without an iCalendar parser.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the façade over the three tables. It carries the dialect (for `?`→`$n` rebinding) and a clock seam for tests.
func NewStore ¶
NewStore builds a Store over the opened DB and its dialect. A nil dialect defaults to SQLite (the in-tree default).
func Open ¶
Open constructs the named backend. Unknown name is a startup error listing what is available (so a misconfigured --calendar-store fails loudly).
func (*Store) CalendarByFeedHash ¶
CalendarByFeedHash resolves an ICS feed token (already hashed) to its live calendar.
func (*Store) DeleteObject ¶
DeleteObject tombstones the live object under name. Returns its etag and whether it existed.
func (*Store) EnsureCalendar ¶
EnsureCalendar returns the live calendar (tenant, username, name), creating it when absent. On an existing calendar every non-empty display field of c (DisplayName, Description, Color, Timezone, Policy) and a non-zero SortOrder are applied; empty ones are left unchanged. A soft-deleted calendar of the same name is resurrected (its objects stay tombstoned). created reports a fresh row.
func (*Store) EnsureSchema ¶
EnsureSchema creates the tables + indexes if absent. Portable DDL: TEXT ids (hxid), TEXT RFC3339 timestamps, JSON as TEXT, native partial indexes, BIGINT counters — one DDL serves SQLite and Postgres.
func (*Store) GetAccount ¶
GetAccount looks an account up by username (the login identity).
func (*Store) GetCalendar ¶
func (s *Store) GetCalendar(ctx context.Context, tenant, username, name string) (Calendar, bool, error)
GetCalendar returns the live calendar (tenant, username, name).
func (*Store) GetCalendarByID ¶
GetCalendarByID returns a live calendar by id.
func (*Store) GetObjectByUID ¶
GetObjectByUID returns the live object with this UID.
func (*Store) ListCalendars ¶
ListCalendars returns the account's live calendars, by sort order then name.
func (*Store) ListObjects ¶
ListObjects returns a calendar's objects (live only unless IncludeDeleted), optionally after a modseq and/or restricted to names, by name.
func (*Store) ObjectsInRange ¶
func (s *Store) ObjectsInRange(ctx context.Context, calID string, start, end time.Time) ([]Object, error)
ObjectsInRange is the coarse time-range prefilter: recurring objects and objects with unknown bounds always pass; the rest by UTC overlap. The caller applies the exact match (recurrence expansion) on the result.
func (*Store) PutEvent ¶
func (s *Store) PutEvent(ctx context.Context, calID, username, name string, ev Event, now time.Time) (PutResult, error)
PutEvent materializes ev into the calendar, addressed by UID: the chassis renders it, an existing object with that UID is updated in place under its own resource name (name is used only on create; "" derives one from the UID), unchanged content is a no-op, and changed content gets a SEQUENCE above the stored one. ev.UID "" derives DefaultUID(name, username). The one write path txco://calendar/put and the CALENDARS/ seed share.
func (*Store) PutICal ¶
PutICal materializes iCalendar text (canonicalized) into the calendar, addressed by the bytes' own UID.
func (*Store) PutObject ¶
func (s *Store) PutObject(ctx context.Context, calID string, obj Object, opts PutOpts) (PutResult, error)
PutObject stores obj (its ICal is the canonical encoding; Name, UID, Component, Summary, DTStartUTC, DTEndUTC, Recurs, Sequence are the facts the caller parsed from it). The calendar's sync_token advances with the object's modseq in one transaction. See the package comment for the identity, no-op and tombstone rules.
func (*Store) RemoveCalendar ¶
RemoveCalendar soft-deletes a calendar and tombstones its live objects.
func (*Store) SetCalendarProps ¶
func (s *Store) SetCalendarProps(ctx context.Context, id string, displayName, description, color *string, sortOrder *int) error
SetCalendarProps applies a client PROPPATCH: only the fields whose pointer is non-nil change (an empty string clears).
func (*Store) SetFeedToken ¶
SetFeedToken stores the sha256 hex of a feed token ("" disables the feed).
func (*Store) UpsertAccount ¶
func (s *Store) UpsertAccount(ctx context.Context, tenant, username, status string, policy json.RawMessage) (created bool, err error)
UpsertAccount creates the account or updates it. An empty status / policy leaves the stored value unchanged on update. created reports whether the row was new. The account holds no password: who may sign in as it is the identity store's business (chassis/authn — a binding and a credential).