Help us shape the future of Go! Take the 2026 Go Developer Survey

calendar

package
v0.2.49 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 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