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

exportstream

package
v0.3.3 Latest Latest
Warning

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

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

Documentation

Overview

Package exportstream is what every Export stream shares (ADR 0045, GLOSSARY: Export stream): the opaque (updated_at, id) cursor, the keyset query predicates, the page size, the NDJSON page encoder and the check of an encoded row against the TypeSpec row schema. A stream adds only its row type (with Position), its fetch query and its operation; see events.go for the smallest example.

Core code: the license gate is the caller's (internal/api/external asks the Edition), nothing here knows about licenses.

Index

Constants

View Source
const AuditSchema = "ExportAuditRow"

AuditSchema is the TypeSpec row schema (openapi component) an AuditRow encodes to.

View Source
const ContactsSchema = "ExportContactRow"

ContactsSchema is the TypeSpec row schema (openapi component) a ContactRow encodes to.

View Source
const ErasuresSchema = "ExportErasureRow"

ErasuresSchema is the TypeSpec row schema (openapi component) an ErasureRow encodes to.

View Source
const EventsSchema = "ExportEventRow"

EventsSchema is the TypeSpec row schema (openapi component) an EventRow encodes to.

View Source
const MessagesSchema = "ExportMessageRow"

MessagesSchema is the TypeSpec row schema (openapi component) a MessageRow encodes to.

Variables

View Source
var ErrBadCursor = errors.New("exportstream: invalid cursor")

ErrBadCursor means a cursor token is not one this package issued: a malformed or foreign value is refused (400) instead of silently restarting a consumer from zero.

Functions

func Order

func Order(s *sql.Selector)

Order is the order of a stream's query: oldest change first, ties by id.

func Seek

func Seek(after Cursor) func(*sql.Selector)

Seek is the keyset predicate of a stream's query: rows at or after the cursor. Pass it to an ent query's Where as a predicate of that entity (`predicate.Event(exportstream.Seek(c))`); it adds nothing for the beginning of the stream. It reads the entity's `updated_at` and `id` columns, which the index on (workspace_id, updated_at, id) covers.

func ValidateRow

func ValidateRow(schema string, line []byte) error

ValidateRow reports why one encoded row (a line of a page, without its newline) does not match the TypeSpec row schema named by schema (an openapi component such as EventsSchema), or nil when it does. It is the check that keeps a stream's row type and its published contract the same thing: beyond the schema's own types and required keys, a key the schema does not declare is a failure too, because the generated documentation would not describe it.

Types

type AuditRow

type AuditRow struct {
	ID         int64                      `json:"id,string"`
	OccurredAt time.Time                  `json:"occurred_at"`
	ActorKind  string                     `json:"actor_kind"`
	ActorID    *string                    `json:"actor_id"`
	ActorName  *string                    `json:"actor_name"`
	Action     string                     `json:"action"`
	TargetType string                     `json:"target_type"`
	TargetID   *string                    `json:"target_id"`
	TargetName *string                    `json:"target_name"`
	Diff       map[string]json.RawMessage `json:"diff"`
	RequestID  *string                    `json:"request_id"`
	IP         *string                    `json:"ip"`
	UserAgent  *string                    `json:"user_agent"`
	CreatedAt  time.Time                  `json:"created_at"`
	UpdatedAt  time.Time                  `json:"updated_at"`
}

AuditRow is one line of the Audit stream, the contract's ExportAuditRow: every key is always present, an absent value is null, ids are decimal strings like everywhere in /api. Entries are append-only, so a row is delivered once.

func (AuditRow) Position

func (r AuditRow) Position() Cursor

Position is the row's (updated_at, id).

type ContactRow

type ContactRow struct {
	ID           int64          `json:"id,string"`
	SubjectID    *string        `json:"subject_id"`
	Email        *string        `json:"email"`
	Phone        *string        `json:"phone"`
	FirstName    *string        `json:"first_name"`
	LastName     *string        `json:"last_name"`
	TimeZone     *string        `json:"time_zone"`
	CustomFields map[string]any `json:"custom_fields"`
	Tags         []string       `json:"tags"`
	CreatedAt    time.Time      `json:"created_at"`
	UpdatedAt    time.Time      `json:"updated_at"`
}

ContactRow is one line of the Contacts stream, the contract's ExportContactRow: every key is always present, an absent value is null, Custom fields are one JSON column and the Tags are their names.

func (ContactRow) Position

func (r ContactRow) Position() Cursor

Position is the row's (updated_at, id).

type Cursor

type Cursor struct {
	At time.Time
	ID int64
}

Cursor is a position in a stream: every row at or after (At, ID) in (updated_at, id) order. The zero Cursor is the beginning of the stream. It is inclusive, and the cursor a page hands back is the position right after its last row (Following), so a row that shares its updated_at with the last row of a page is never skipped and never repeated.

func ParseCursor

func ParseCursor(token string) (Cursor, error)

ParseCursor reads a token String issued. An empty token is the beginning of the stream; anything else that String could not have produced is ErrBadCursor.

func (Cursor) Following

func (c Cursor) Following() Cursor

Following is the position right after c: ids are integers, so the successor of (updated_at, id) among the rows of one instant is (updated_at, id+1). The instant is cut to microseconds, the precision Postgres stores.

func (Cursor) IsZero

func (c Cursor) IsZero() bool

IsZero reports whether c is the beginning of the stream.

func (Cursor) String

func (c Cursor) String() string

String is the opaque token of c, empty for the beginning of the stream.

type ErasureRow

type ErasureRow struct {
	ID         int64     `json:"id,string"`
	ContactID  *int64    `json:"contact_id,string"`
	SubjectID  string    `json:"subject_id"`
	VisitorIDs []string  `json:"visitor_ids"`
	EmailHash  *string   `json:"email_hash"`
	PhoneHash  *string   `json:"phone_hash"`
	ErasedAt   time.Time `json:"erased_at"`
	CreatedAt  time.Time `json:"created_at"`
	UpdatedAt  time.Time `json:"updated_at"`
}

ErasureRow is one line of the Erasures stream, the contract's ExportErasureRow: the keys an Erasure matched on. Every key is always present, an absent value is null, ids are decimal strings like everywhere in /api. The hashes are SHA-256 as lowercase hex; the address and the phone themselves are never stored, so they cannot be here.

func (ErasureRow) Position

func (r ErasureRow) Position() Cursor

Position is the row's (updated_at, id).

type EventRow

type EventRow struct {
	ID         int64          `json:"id,string"`
	Action     string         `json:"action"`
	Properties map[string]any `json:"properties"`
	ContactID  *int64         `json:"contact_id,string"`
	VisitorID  *string        `json:"visitor_id"`
	SubjectID  string         `json:"subject_id"`
	Email      *string        `json:"email"`
	Phone      *string        `json:"phone"`
	OccurredAt *time.Time     `json:"occurred_at"`
	CreatedAt  time.Time      `json:"created_at"`
	UpdatedAt  time.Time      `json:"updated_at"`
}

EventRow is one line of the Events stream, the contract's ExportEventRow: every key is always present, an absent value is null, ids are decimal strings like everywhere in /api.

func (EventRow) Position

func (r EventRow) Position() Cursor

Position is the row's (updated_at, id).

type MessageRow

type MessageRow struct {
	ID                int64      `json:"id,string"`
	Kind              string     `json:"kind"`
	Channel           string     `json:"channel"`
	Destination       *string    `json:"destination"`
	Status            string     `json:"status"`
	Reason            *string    `json:"reason"`
	ContactID         *int64     `json:"contact_id,string"`
	BroadcastID       *int64     `json:"broadcast_id,string"`
	AutomationID      *int64     `json:"automation_id,string"`
	AutomationRunID   *int64     `json:"automation_run_id,string"`
	AutomationStep    *int       `json:"automation_step"`
	IntegrationID     *int64     `json:"integration_id,string"`
	TemplateID        *int64     `json:"template_id,string"`
	SendingSource     *string    `json:"sending_source"`
	SendingDomain     *string    `json:"sending_domain"`
	ProviderMessageID *string    `json:"provider_message_id"`
	ClaimedAt         time.Time  `json:"claimed_at"`
	SentAt            *time.Time `json:"sent_at"`
	DeliveredAt       *time.Time `json:"delivered_at"`
	BouncedAt         *time.Time `json:"bounced_at"`
	ComplainedAt      *time.Time `json:"complained_at"`
	Captured          bool       `json:"captured"`
	CreatedAt         time.Time  `json:"created_at"`
	UpdatedAt         time.Time  `json:"updated_at"`
}

MessageRow is one line of the Outbound messages stream, the contract's ExportMessageRow: delivery provenance and outcome, never a subject or a body. Every key is always present, an absent value is null, ids are decimal strings like everywhere in /api.

func (MessageRow) Position

func (r MessageRow) Position() Cursor

Position is the row's (updated_at, id).

type Page

type Page struct {
	Cursor string
	Body   io.Reader
}

Page is one answer of a stream: the body (NDJSON, one row per line, no envelope) and the token of the cursor for the next request.

func Audit

func Audit(ctx context.Context, s *ent.Scoped, token string, limit int) (Page, error)

Audit serves a page of the Workspace's Audit stream. The caller has checked the `audit` license, which is an Enterprise feature of the entries themselves.

func Contacts

func Contacts(ctx context.Context, s *ent.Scoped, token string, limit int) (Page, error)

Contacts serves a page of the Workspace's Contacts stream. A Tag change moves the Contact's updated_at (internal/tags), so the Tag names ride the row's own version.

func Erasures

func Erasures(ctx context.Context, s *ent.Scoped, token string, limit int) (Page, error)

Erasures serves a page of the Workspace's Erasures stream.

func Events

func Events(ctx context.Context, s *ent.Scoped, token string, limit int) (Page, error)

Events serves a page of the Workspace's Events stream.

func Messages

func Messages(ctx context.Context, s *ent.Scoped, token string, limit int) (Page, error)

Messages serves a page of the Workspace's Outbound messages stream.

func Read

func Read[R Row](token string, limit int, fetch func(after Cursor, limit int) ([]R, error)) (Page, error)

Read serves one page of a stream. token is the request's cursor; fetch returns the rows at or after a cursor in (updated_at, id) order, at most limit of them. The page's cursor is the position after the last row, or token itself when the page is empty, so a poller can loop on it without special cases. A malformed token is ErrBadCursor.

type Row

type Row interface {
	// Position is the row's own (updated_at, id).
	Position() Cursor
}

Row is one line of a stream: a flat struct whose JSON is the TypeSpec row schema, and which knows its own position.

Jump to

Keyboard shortcuts

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