Documentation
¶
Overview ¶
Package contacts is the durable contacts store behind the chassis `contacts` personality (CardDAV) and the txco://contacts/* ops: accounts, per-account address books, and per-book vCard 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, imap and calendar 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 address book. A stack put addresses BY UID and keeps whatever resource name the object already has, so a client-created card and its later re-materializations are one object. Unlike the calendar store, the stored bytes are the client's own (CRLF-normalized) — vCard has no sound canonical encoder in the library stack, and a contacts app expects to read back exactly what it wrote (photos, X- properties, groups). The ETag is their sha256; a put whose content differs only in REV or PRODID is a no-op, so a 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 DefaultObjectName(uid string) string
- func DefaultUID(name, username string) string
- func ETagOf(vcard []byte) string
- func Normalize(b []byte) []byte
- func NormalizeUsername(u string) string
- func PolicyMode(ab *Addressbook, acct *Account, verb string) string
- func Register(name string, c Constructor)
- func Render(c Card, now time.Time) ([]byte, error)
- func SameContent(a, b []byte) bool
- func ToLibrary(b []byte) (vcard.Card, error)
- func ValidAddressbookName(name string) bool
- func ValidMode(s string) bool
- func ValidObjectName(name string) bool
- func ValidatePolicy(raw json.RawMessage) error
- type Account
- type Addressbook
- type BatchError
- type BatchResult
- type Card
- type Config
- type Constructor
- type ListOpts
- type Name
- type Object
- type PutOpts
- type PutResult
- type Store
- func (s *Store) Batch(ctx context.Context, abID string, puts []Object, deletes []string) (BatchResult, error)
- func (s *Store) Close() error
- func (s *Store) DB() *sql.DB
- func (s *Store) DeleteObject(ctx context.Context, abID, name string) (string, bool, error)
- func (s *Store) EnsureAddressbook(ctx context.Context, ab Addressbook) (Addressbook, 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) GetAddressbook(ctx context.Context, tenant, username, name string) (Addressbook, bool, error)
- func (s *Store) GetAddressbookByID(ctx context.Context, id string) (Addressbook, bool, error)
- func (s *Store) GetObject(ctx context.Context, abID, name string) (Object, bool, error)
- func (s *Store) GetObjectByUID(ctx context.Context, abID, uid string) (Object, bool, error)
- func (s *Store) ListAddressbooks(ctx context.Context, tenant, username string) ([]Addressbook, error)
- func (s *Store) ListObjects(ctx context.Context, abID string, opts ListOpts) ([]Object, error)
- func (s *Store) PutCard(ctx context.Context, abID, username, name string, c Card, now time.Time) (PutResult, error)
- func (s *Store) PutObject(ctx context.Context, abID string, obj Object, opts PutOpts) (PutResult, error)
- func (s *Store) PutVCard(ctx context.Context, abID, name string, raw []byte) (PutResult, error)
- func (s *Store) RemoveAddressbook(ctx context.Context, id string) (bool, error)
- func (s *Store) SetAddressbookProps(ctx context.Context, id string, displayName, description *string, ...) error
- func (s *Store) SetClock(now func() time.Time)
- func (s *Store) UpsertAccount(ctx context.Context, tenant, username, pwHash, status string, ...) (created bool, err error)
- type Typed
Constants ¶
const ( VerbPut = "put" VerbDelete = "delete" VerbMkaddressbook = "mkaddressbook" VerbRemove = "remove" VerbProppatch = "proppatch" ModeDeny = "deny" ModeLocal = "local" ModeObserve = "observe" ModeStack = "stack" )
Policy vocabulary: per address book, five verbs, each one of four modes. The chassis has no opinion about what an address book holds; it has one about which client mutations a stack hears of, and when. Same vocabulary as the calendar personality, with `mkaddressbook` for `mkcalendar`.
deny refused at the protocol layer (403), no round trip local protocol state only, no event observe commit, then tell the `_contacts` stack (fire-and-forget) stack ask the `_contacts` stack first; absent/false @contacts.res.ok ⇒ 403
Resolution: the address book's own policy, then the account's default policy, then the chassis default — `put`/`delete` observe, `proppatch` local (display name / description changes), `mkaddressbook`/`remove` deny (address books are provisioned by ops unless a product opens that).
const ( StatusActive = "active" StatusDisabled = "disabled" )
Account statuses.
const ProdID = "-//loremlabs//txco contacts//EN"
ProdID identifies chassis-rendered vCards.
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("contacts: username belongs to another tenant") // ErrPrecondition is a failed If-Match / If-None-Match on a put. ErrPrecondition = errors.New("contacts: precondition failed") // ErrUIDConflict is a put whose UID is already held by another live // object in the same address book. ErrUIDConflict = errors.New("contacts: uid belongs to another object") // ErrNotFound is a missing address book or object. ErrNotFound = errors.New("contacts: not found") )
var ErrInvalidCard = errors.New("contacts: invalid card")
ErrInvalidCard wraps a validation failure of the card or its bytes, so callers can answer "invalid argument" rather than "store failure".
var ErrUnsupportedVersion = errors.New("vcard: VERSION is not 3.0 or 4.0")
ErrUnsupportedVersion is a vCard whose VERSION is not 3.0 or 4.0 (the CardDAV `supported-address-data` precondition).
var PolicyVerbs = []string{VerbPut, VerbDelete, VerbMkaddressbook, 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 .vcf>.<local>@<domain> — stable across re-materializations, unique per persona. Stacks that spell a UID out must derive the same value.
func Normalize ¶
Normalize is all a client's bytes get before storage: BOM dropped, line endings CRLF, a trailing CRLF.
func NormalizeUsername ¶
NormalizeUsername lowercases and trims a login / op username.
func PolicyMode ¶
func PolicyMode(ab *Addressbook, acct *Account, verb string) string
PolicyMode resolves one verb for an address book (nil for a top-level MKCOL, 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 c as a vCard 3.0 (the RFC 6352 baseline every contacts app reads): properties in a fixed order, text escaped, lines folded, CRLF. Deterministic apart from REV, which the store's no-op rule ignores.
func SameContent ¶
SameContent reports whether two vCards differ only in their REV / PRODID lines — the re-materialization no-op rule. Folded continuation lines are joined before the comparison.
func ToLibrary ¶
ToLibrary re-reads the bytes as a go-vcard Card for go-webdav's listing and query paths (which encode it with go-vcard's encoder). Values are unescaped the way that encoder expects to re-escape them; an escaped `\;` becomes a bare `;` (that encoder never escapes semicolons).
func ValidAddressbookName ¶
ValidAddressbookName reports whether name is an address book path segment: a URL segment of up to 128 chars (clients mint UUIDs; ops use short lowercase names).
func ValidObjectName ¶
ValidObjectName reports whether name is an object resource name (a URL segment; the conventional `.vcf` 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
PwHash string
Status string
Policy json.RawMessage
CreatedAt time.Time
UpdatedAt time.Time
}
Account is a contacts_accounts row.
type Addressbook ¶
type Addressbook struct {
ID string
Tenant string
Username string
Name string
DisplayName string
Description string
SortOrder int
Policy json.RawMessage
SyncToken int64
CreatedAt time.Time
UpdatedAt time.Time
}
Addressbook is a live contacts_addressbooks row.
type BatchError ¶
BatchError names the entry that failed a Batch (nothing was written).
func (*BatchError) Error ¶
func (e *BatchError) Error() string
func (*BatchError) Unwrap ¶
func (e *BatchError) Unwrap() error
type BatchResult ¶
type BatchResult struct {
Created int
Updated int
Noop int
Deleted int
Missing int // deletes whose uid had no live object
Puts []PutResult
}
BatchResult reports what Batch did.
type Card ¶
type Card struct {
UID string `json:"uid,omitempty"`
FN string `json:"fn,omitempty"`
Name *Name `json:"name,omitempty"`
Nickname string `json:"nickname,omitempty"`
Org string `json:"org,omitempty"`
Title string `json:"title,omitempty"`
Note string `json:"note,omitempty"`
URL string `json:"url,omitempty"`
Birthday string `json:"birthday,omitempty"`
Emails []Typed `json:"emails,omitempty"`
Phones []Typed `json:"phones,omitempty"`
Kind string `json:"kind,omitempty"`
Members []string `json:"members,omitempty"`
// Facts the chassis fills in when parsing; ignored when rendering.
Version string `json:"version,omitempty"`
Rev string `json:"rev,omitempty"`
Addresses []string `json:"addresses"` // EMAIL values, lowercased, deduped, in order
}
Card is the chassis's structured form of one vCard: generic vCard vocabulary, no product words. On the way IN (txco://contacts/put WITH card, @contacts.res.card) the first block is read; on the way OUT (@contacts.card, txco://contacts/get) the facts are filled in as well. `kind` is individual (default), group, org or location; a group's `members` are the member UIDs as the client wrote them (urn:uuid:…).
func CardFromJSON ¶
CardFromJSON reads a Card from its JSON form (the WITH card{} param or a stack's @contacts.res.card).
type Config ¶
type Config struct {
// DBPath is the bundled SQLite backend's file path (--contacts-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 calendar.Config / imap.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 Name ¶
type Name struct {
Family string `json:"family,omitempty"`
Given string `json:"given,omitempty"`
Additional string `json:"additional,omitempty"`
Prefix string `json:"prefix,omitempty"`
Suffix string `json:"suffix,omitempty"`
}
Name is the structured N property.
type Object ¶
type Object struct {
AddressbookID string
Name string
UID string
ETag string
VCard []byte
Size int64
Version string // "3.0" | "4.0"
FN string // the formatted name
Kind string // individual | group | org | location
Addresses []string // EMAIL values, lowercased, deduped, in order
ModSeq int64
CreatedAt time.Time
UpdatedAt time.Time
Deleted bool
}
Object is a contacts_objects row.
func ObjectFromCard ¶
ObjectFromCard renders c (vCard 3.0) into a storable Object. c.UID "" derives DefaultUID(name, username), so name is then required. name "" derives DefaultObjectName(uid).
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 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 --contacts-store fails loudly).
func (*Store) Batch ¶
func (s *Store) Batch(ctx context.Context, abID string, puts []Object, deletes []string) (BatchResult, error)
Batch applies puts (each addressed by UID, the stack's mode) and deletes (by UID) in ONE transaction with one sync_token advance: the bounded write a stack without loops needs to reconcile a list. An error on any entry rolls the whole batch back and names the entry (*BatchError). Unknown uids in deletes are counted as Missing, not errors.
func (*Store) DeleteObject ¶
DeleteObject tombstones the live object under name. Returns its etag and whether it existed.
func (*Store) EnsureAddressbook ¶
func (s *Store) EnsureAddressbook(ctx context.Context, ab Addressbook) (Addressbook, bool, error)
EnsureAddressbook returns the live address book (tenant, username, name), creating it when absent. On an existing book every non-empty display field of ab (DisplayName, Description, Policy) and a non-zero SortOrder are applied; empty ones are left unchanged. A soft-deleted book 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) GetAddressbook ¶
func (s *Store) GetAddressbook(ctx context.Context, tenant, username, name string) (Addressbook, bool, error)
GetAddressbook returns the live address book (tenant, username, name).
func (*Store) GetAddressbookByID ¶
GetAddressbookByID returns a live address book by id.
func (*Store) GetObjectByUID ¶
GetObjectByUID returns the live object with this UID.
func (*Store) ListAddressbooks ¶
func (s *Store) ListAddressbooks(ctx context.Context, tenant, username string) ([]Addressbook, error)
ListAddressbooks returns the account's live address books, by sort order then name.
func (*Store) ListObjects ¶
ListObjects returns an address book's objects (live only unless IncludeDeleted), optionally after a modseq and/or restricted to names, by name.
func (*Store) PutCard ¶
func (s *Store) PutCard(ctx context.Context, abID, username, name string, c Card, now time.Time) (PutResult, error)
PutCard materializes c into the address book, addressed by UID: an existing object with that UID is updated in place under its own resource name (name is used only on create), unchanged content is a no-op. The write path txco://contacts/put and the CONTACTS/ seed share.
func (*Store) PutObject ¶
func (s *Store) PutObject(ctx context.Context, abID string, obj Object, opts PutOpts) (PutResult, error)
PutObject stores obj (its VCard is the bytes to keep; Name, UID, Version, FN, Kind, Addresses are the facts the caller parsed from them). The address book'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) PutVCard ¶
PutVCard materializes raw vCard text into the address book, addressed by the bytes' own UID.
func (*Store) RemoveAddressbook ¶
RemoveAddressbook soft-deletes an address book and tombstones its live objects.
func (*Store) SetAddressbookProps ¶
func (s *Store) SetAddressbookProps(ctx context.Context, id string, displayName, description *string, sortOrder *int) error
SetAddressbookProps applies a client PROPPATCH: only the fields whose pointer is non-nil change (an empty string clears).
func (*Store) UpsertAccount ¶
func (s *Store) UpsertAccount(ctx context.Context, tenant, username, pwHash, status string, policy json.RawMessage) (created bool, err error)
UpsertAccount creates the account or updates it. An empty pwHash / status / policy leaves the stored value unchanged on update; pwHash is required on create. created reports whether the row was new.