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

contacts

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: Oct 25, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

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

View Source
const (
	FieldEmail     = "email"
	FieldPhone     = "phone"
	FieldSubjectID = "subject_id"
)

Alias key fields, as reported by ConflictError.Field.

Variables

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

View Source
var ErrNotFound = errors.New("contacts: contact not found")

ErrNotFound means the Contact does not exist in the Workspace.

Functions

func CustomFields

func CustomFields(ctx context.Context, s *ent.Scoped) ([]*ent.CustomField, error)

CustomFields returns the Workspace's whole Custom field catalogue, ascending by key. The catalogue is small and unpaginated.

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

type BatchOutcome struct {
	Result Result
	Err    error
}

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 New

func New(bus *events.Bus) *Module

New builds the module.

func (*Module) Create

func (m *Module) Create(ctx context.Context, s *ent.Scoped, src contact.Source, attrs Attributes) (*ent.Contact, error)

Create makes a Contact from the given door (its Contact source, kept for good) and publishes contact.created in one transaction. An alias key already taken in the Workspace yields a *ConflictError.

func (*Module) Get

func (m *Module) Get(ctx context.Context, s *ent.Scoped, id int64) (*ent.Contact, error)

Get returns the Contact with its Tags loaded, sorted by name, or ErrNotFound.

func (*Module) List

List returns one page of the Workspace's Contacts, ascending by id, narrowed by f.

func (*Module) RecordImport

func (m *Module) RecordImport(ctx context.Context, s *ent.Scoped, sum ImportSummary) error

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

func (m *Module) Upsert(ctx context.Context, s *ent.Scoped, src contact.Source, attrs Attributes) (Result, error)

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), and its Contact source stays what it was: src only names the door of a Contact this call creates. ErrIdentityRequired when no alias key is given.

func (*Module) UpsertBatch

func (m *Module) UpsertBatch(ctx context.Context, s *ent.Scoped, src contact.Source, 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, src contact.Source, 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.

Jump to

Keyboard shortcuts

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