calendar

package
v0.2.40 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MPL-2.0 Imports: 21 Imported by: 0

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

View Source
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).

View Source
const (
	StatusActive   = "active"
	StatusDisabled = "disabled"
)

Account statuses.

View Source
const ProdID = "-//loremlabs//txco calendar//EN"

ProdID identifies chassis-rendered calendars.

Variables

View Source
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")
)
View Source
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".

PolicyVerbs lists the verbs a policy object may name.

Functions

func Canonical

func Canonical(b []byte) ([]byte, error)

Canonical re-encodes bytes into the canonical form.

func Decode

func Decode(b []byte) (*ical.Calendar, error)

Decode parses iCalendar bytes.

func DefaultObjectName

func DefaultObjectName(uid string) string

DefaultObjectName derives a resource name from a UID's local part.

func DefaultUID

func DefaultUID(name, username string) string

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 ETagOf

func ETagOf(ical []byte) string

ETagOf is the etag of a canonical encoding: bare sha256 hex.

func Encode

func Encode(cal *ical.Calendar) ([]byte, error)

Encode is the canonical encoding: go-ical's encoder (sorted property names, children in order, 75-octet folding, CRLF).

func FeedBytes

func FeedBytes(name, description string, objs []Object) ([]byte, error)

FeedBytes renders an ICS subscription feed: one VCALENDAR carrying every live object's components, VTIMEZONEs de-duplicated by TZID.

func Master

func Master(cal *ical.Calendar) (master *ical.Component, overrides []*ical.Component, err error)

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

func NormalizeUsername(u string) string

NormalizeUsername lowercases and trims a login / op username.

func PolicyMode

func PolicyMode(cal *Calendar, acct *Account, verb string) string

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

func Render(ev Event, now time.Time) ([]byte, error)

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

func SameContent(a, b []byte) bool

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

func VTimezone(loc *time.Location, now time.Time) *ical.Component

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

func ValidCalendarName(name string) bool

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 ValidMode

func ValidMode(s string) bool

ValidMode reports whether s is a policy mode.

func ValidObjectName

func ValidObjectName(name string) bool

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

type Constructor func(Config) (*Store, error)

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

func EventFromJSON(raw []byte) (Event, error)

EventFromJSON reads an Event from its JSON form (the WITH event{} param or a stack's @calendar.res.event).

func Parse

func Parse(b []byte) (Event, error)

Parse reads the facts of a calendar object: the master component's Event with every derived field filled in. The bytes need not be canonical.

func ParseCalendar

func ParseCalendar(cal *ical.Calendar) (Event, error)

ParseCalendar is Parse over a decoded calendar.

type ListOpts

type ListOpts struct {
	SinceModSeq    int64
	IncludeDeleted bool
	Names          []string
}

ListOpts narrows ListObjects.

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

func NewStore(db *sql.DB, d registry.Dialect) *Store

NewStore builds a Store over the opened DB and its dialect. A nil dialect defaults to SQLite (the in-tree default).

func Open

func Open(name string, cfg Config) (*Store, error)

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

func (s *Store) CalendarByFeedHash(ctx context.Context, hash string) (Calendar, bool, error)

CalendarByFeedHash resolves an ICS feed token (already hashed) to its live calendar.

func (*Store) Close

func (s *Store) Close() error

Close releases the underlying DB handle.

func (*Store) DB

func (s *Store) DB() *sql.DB

DB exposes the handle for tests and backends. Not for request paths.

func (*Store) DeleteObject

func (s *Store) DeleteObject(ctx context.Context, calID, name string) (string, bool, error)

DeleteObject tombstones the live object under name. Returns its etag and whether it existed.

func (*Store) EnsureCalendar

func (s *Store) EnsureCalendar(ctx context.Context, c Calendar) (Calendar, bool, error)

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

func (s *Store) EnsureSchema(ctx context.Context) error

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

func (s *Store) GetAccount(ctx context.Context, username string) (Account, bool, error)

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

func (s *Store) GetCalendarByID(ctx context.Context, id string) (Calendar, bool, error)

GetCalendarByID returns a live calendar by id.

func (*Store) GetObject

func (s *Store) GetObject(ctx context.Context, calID, name string) (Object, bool, error)

GetObject returns the live object under a resource name.

func (*Store) GetObjectByUID

func (s *Store) GetObjectByUID(ctx context.Context, calID, uid string) (Object, bool, error)

GetObjectByUID returns the live object with this UID.

func (*Store) ListCalendars

func (s *Store) ListCalendars(ctx context.Context, tenant, username string) ([]Calendar, error)

ListCalendars returns the account's live calendars, by sort order then name.

func (*Store) ListObjects

func (s *Store) ListObjects(ctx context.Context, calID string, opts ListOpts) ([]Object, error)

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

func (s *Store) PutICal(ctx context.Context, calID, name string, ical []byte) (PutResult, error)

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

func (s *Store) RemoveCalendar(ctx context.Context, id string) (bool, error)

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) SetClock

func (s *Store) SetClock(now func() time.Time)

SetClock pins the store's clock (tests).

func (*Store) SetFeedToken

func (s *Store) SetFeedToken(ctx context.Context, id, hash string) error

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).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL