Documentation
¶
Overview ¶
Package contacts is the one place a Contact is created, updated or upserted (ADR 0016). Every surface — the /site SPA API, the external /api, the tracker's Identify — hands it Attributes and gets back a Contact or a domain error; none re-implements what lies between "this person is wanted" and "the row is committed":
- alias-key normalization and resolution (subject_id, email, phone — ADR 0002);
- declared-by-use Custom fields (ADR 0006);
- the transaction, and the contact.created event published inside it so the event is committed iff the row is (Automations enroll off it);
- domain errors (ErrNotFound, *ConflictError, ErrIdentityRequired) that each surface maps to its own transport — scope checks and RFC 7807 stay in the adapters.
There is no repository layer over ent: the module uses the Workspace-scoped ent client (ADR 0017) it is handed, and writes no Workspace predicate.
Index ¶
- Constants
- Variables
- func EnsureCustomFields(ctx context.Context, s *ent.Scoped, kv map[string]any) (map[string]any, error)
- func Resolve(ctx context.Context, s *ent.Scoped, subjectID, email, phone *string) (*ent.Contact, error)
- func ResolveID(ctx context.Context, s *ent.Scoped, subjectID string, email, phone *string) (int64, error)
- type Attributes
- type BatchOutcome
- type Cleared
- type ConflictError
- type Filter
- type ImportSummary
- type Module
- func (m *Module) Create(ctx context.Context, s *ent.Scoped, attrs Attributes) (*ent.Contact, error)
- func (m *Module) List(ctx context.Context, s *ent.Scoped, p pagination.Params, f Filter) (pagination.Page[*ent.Contact], error)
- func (m *Module) RecordImport(ctx context.Context, s *ent.Scoped, sum ImportSummary) error
- func (m *Module) Update(ctx context.Context, s *ent.Scoped, id int64, attrs Attributes) (*ent.Contact, error)
- func (m *Module) Upsert(ctx context.Context, s *ent.Scoped, attrs Attributes) (Result, error)
- func (m *Module) UpsertBatch(ctx context.Context, s *ent.Scoped, items []Attributes) []BatchOutcome
- type Result
Constants ¶
const ( FieldEmail = "email" FieldPhone = "phone" FieldSubjectID = "subject_id" )
Alias key fields, as reported by ConflictError.Field.
Variables ¶
var ErrIdentityRequired = errors.New("contacts: subject_id, email or phone is required")
ErrIdentityRequired means an upsert was given none of the alias keys, so there is nothing to resolve or anchor a Contact by.
var ErrNotFound = errors.New("contacts: contact not found")
ErrNotFound means the Contact does not exist in the Workspace.
Functions ¶
func EnsureCustomFields ¶
func EnsureCustomFields(ctx context.Context, s *ent.Scoped, kv map[string]any) (map[string]any, error)
EnsureCustomFields implements declared-by-use Custom fields (ADR 0006): for each key in the incoming attribute map it auto-creates a typed CustomField definition (idempotent upsert on (workspace_id, key)) and returns the values to merge into a Contact's custom_fields. There is no schemaless "trait" — every attribute is a first-class, typed, renameable definition from the first sight.
Type is inferred from the value's JSON shape on first sight. A later value of a conflicting type does NOT retype or reject (declared-by-use must not break ingest); the stored value keeps its own JSON type and the definition's type is left as-is — widening/coercion policy is a later refinement.
func Resolve ¶
func Resolve(ctx context.Context, s *ent.Scoped, subjectID, email, phone *string) (*ent.Contact, error)
Resolve finds an existing Contact by any present alias key (subject_id → email → phone), or returns nil. It never creates. Keys are matched as normalized by Attributes, so callers may pass raw values.
func ResolveID ¶
func ResolveID(ctx context.Context, s *ent.Scoped, subjectID string, email, phone *string) (int64, error)
ResolveID resolves an existing Contact by any present alias key (subject_id → email → phone) and returns its id, or 0 if none matches. It never creates a Contact — used by event ingest to attach an event to a Contact by stable identity when one already exists, leaving it anonymous (0) otherwise.
Types ¶
type Attributes ¶
type Attributes struct {
SubjectID, Email, Phone *string
FirstName, LastName, TimeZone *string
// CustomFields are declared by use (ADR 0006) and stored typed.
CustomFields map[string]any
// Cleared lists the attributes an Update must clear (JSON Merge Patch: an
// explicit null clears, an absent key keeps). ADR 0002 keeps every alias key
// optional, so clearing even the last one is valid. Ignored by Create and Upsert.
Cleared Cleared
}
Attributes are the writable attributes of a Contact. A nil pointer (or nil CustomFields) means "not given": Create leaves it unset, Update leaves it as is.
type BatchOutcome ¶
BatchOutcome is the result of one item of UpsertBatch: Result on success, Err (a domain error, or a storage error) otherwise.
type Cleared ¶
type Cleared struct {
SubjectID, Email, Phone bool
FirstName, LastName, TimeZone bool
CustomFields bool
}
Cleared flags the attributes an Update clears.
type ConflictError ¶
type ConflictError struct {
// Field is the alias key in conflict (FieldEmail, FieldPhone, FieldSubjectID).
Field string
}
ConflictError means an alias key is already held by another Contact of the Workspace.
func (*ConflictError) Error ¶
func (e *ConflictError) Error() string
func (*ConflictError) Message ¶
func (e *ConflictError) Message() string
Message words the conflict for a human, in the instance locale. The single place the wording lives, so every surface reports the same conflict identically.
type Filter ¶
type Filter struct {
// EmailContains keeps the Contacts whose address contains it, ignoring case.
EmailContains string
}
Filter narrows a list of Contacts; the zero value keeps them all.
type ImportSummary ¶
type ImportSummary struct {
Created, Updated, Failed int
}
ImportSummary counts the rows of one import.
type Module ¶
type Module struct {
// contains filtered or unexported fields
}
Module is the contacts module.
func (*Module) Create ¶
Create makes a Contact and publishes contact.created in one transaction. An alias key already taken in the Workspace yields a *ConflictError.
func (*Module) List ¶
func (m *Module) List(ctx context.Context, s *ent.Scoped, p pagination.Params, f Filter) (pagination.Page[*ent.Contact], error)
List returns one page of the Workspace's Contacts, ascending by id, narrowed by f.
func (*Module) RecordImport ¶
RecordImport records one contact.import Audit entry for a whole import: the actor of s and the row counts, never a Contact or a value. Importers write their rows through an unaudited scope (events.Ingest) so only this entry reaches the log.
func (*Module) Update ¶
func (m *Module) Update(ctx context.Context, s *ent.Scoped, id int64, attrs Attributes) (*ent.Contact, error)
Update changes the given attributes of a Contact; CustomFields, when given, replace the stored set. ErrNotFound when the Contact is not in the Workspace.
func (*Module) Upsert ¶
Upsert resolves the Contact by any present alias key (subject_id, email, phone) or creates one. An existing Contact is only enriched: missing attributes are filled and Custom fields merged, never overwritten (identity is additive). ErrIdentityRequired when no alias key is given.
func (*Module) UpsertBatch ¶
func (m *Module) UpsertBatch(ctx context.Context, s *ent.Scoped, items []Attributes) []BatchOutcome
UpsertBatch upserts each item in its own transaction, in order, so a failing item neither rolls back nor blocks the others; each new Contact publishes contact.created with its own commit. The outcomes are parallel to items.
A batch is an import (ADR 0022): its rows are written through an unaudited scope and the whole batch is recorded as ONE contact.import entry (counts only), not one entry per row. The rows commit one by one (a failing row must not poison the others), so the entry cannot share their transaction: if recording it fails the failure is logged and the committed outcomes are still returned, never a 500 for an import that happened.
type Result ¶
type Result struct {
Contact *ent.Contact
// Created is true when no Contact matched and a new one was made.
Created bool
}
Result is the outcome of an Upsert.
func UpsertIn ¶
func UpsertIn(ctx context.Context, s *ent.Scoped, pub events.Publisher, attrs Attributes) (Result, error)
UpsertIn is Upsert inside a transaction the caller already owns (tx and pub come from events.Bus.WithinScopedTx), for flows that bind more rows to the Contact atomically — the tracker's Identify. It publishes contact.created when it creates.