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

livewire

package module
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Nov 2, 2026 License: Apache-2.0 Imports: 32 Imported by: 0

README

livewire

CI unit tests e2e tests coverage Go Reference License

Coding agents: start at llms.txt and docs/wiki/Home.md; the tested reference application is examples/todo.

Declare an API once in Go and serve it as REST routes on gin, as a WebSocket with reads, live subscriptions and writes fed by PostgreSQL LISTEN/NOTIFY (a committed write re-runs every subscription that reads the table it changed), as generated clients (TypeScript with Vue composables, Go, a CLI) and as an OpenAPI 3.0.3 document.

The registry is the single source of truth; govengers/auth decides who may do what, the same way on both transports. Filterable resources come with filter documents, stored filters, distinct values, cursor pages, item history and a CSV export. Every mutation runs in a transaction the engine owns, with an idempotency record and an audit row when the engine has the IdempotencyStore and the AuditStore. Module github.com/govengers/livewire, Go 1.26, PostgreSQL 18, Apache 2.0; it succeeds github.com/mxcd/livewire v0.2.x (migration).

go get github.com/govengers/livewire

Example

From example_application_test.go, a live, filterable resource and a create:

func declare(todos *todoRepository) (*livewire.Registry, error) {
    registry, err := livewire.NewRegistry(&livewire.RegistryOptions{BasePath: "/api/v1"})
    if err != nil {
        return nil, err
    }
    return registry, registry.Add( // a declaration mistake fails here, at startup (D43)
        livewire.NewResource("todos", "/todos", &livewire.ResourceOptions[todo]{
            Tables: []string{"todos"}, // a committed write to todos re-runs every live read of it
            Read:   authorization.IsUser(),
            Fields: query.Fields{
                {Name: "title", Type: query.TypeText, SQL: "todos.title", Filter: true, Sort: true, Search: true},
                {Name: "done", Type: query.TypeBoolean, SQL: "todos.done", Filter: true, Sort: true},
                {Name: "createdAt", Type: query.TypeTime, SQL: "todos.created_at", Filter: true, Sort: true},
            },
            DefaultSort: []query.SortTerm{{Field: "createdAt", Descending: true}},
            List:        todos.List,
        }),
        livewire.NewMutation("createTodo", http.MethodPost, "/todos", &livewire.MutationOptions[livewire.NoParameters, createTodo, todo]{
            Check: authorization.IsUser(), Status: http.StatusCreated,
            ClientID: livewire.UUIDv7, Effect: livewire.Upserts("todos"), // with `optimistic`, a socket call shows it at once
            Handle: todos.Create,
        }),
    )
}

The engine on the Transactor and Listener of postgresadapter, behind govengers/auth on gin:

engine, err := livewire.NewEngine(&livewire.EngineOptions{
    Registry: registry, ChangeFeed: listener, Transactor: transactor,
    IdentitySource: guard, // re-validates the session of every socket (D6)
    AllowedOrigins: []string{"https://todo.example.com"},
    // IdempotencyStore, AuditStore, FilterStore, PresenceStore: each feature is off without its store
})
if err != nil {
    return err
}
api := router.Group("", authginadapter.Middleware(guard.RequireAPI())) // the identity of every request
if err := ginadapter.RegisterRoutes(api, &ginadapter.Options{Engine: engine}); err != nil {
    return err
}
api.GET("/api/v1/socket", ginadapter.SocketHandler(engine))
err = engine.Run(ctx) // the change feed and the ticks, until ctx ends
closing, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()
return errors.Join(err, engine.Close(closing)) // every socket ends with 1001

The clients, from a generator command the application runs after an API change:

options := &generate.Options{
    TypeScriptDirectory: "web/src/api", // types.ts, filters.ts, api.ts, runtime.ts, vue.ts
    OpenAPIFile:         "openapi.yaml",
    OpenAPI:             generate.OpenAPIOptions{Title: "Todo", Version: "1.0.0"},
}

generate.Write(registry, options) writes them, and livewiretest.GeneratedIsCurrent fails the tests while a committed file is behind. Triggers and tables come from exported SQL in the application's migrations (postgresadapter.TriggerFunctionSQL, then TriggerSQL("todos")).

The packages, what each depends on and which wiki page covers it: docs/wiki/Home.md.

Identity

A client serves one identity for its whole life. The TypeScript Client (REST) and LiveConnection (socket) are a pair (createLivePair) that never switches: another identity, a 4401 close or end() ends both, and every open request fails with an EndedError. Neither sees the session cookie, so the application ends the pair before the request that changes the session (login, logout, switch), then makes the next one and puts it into the ref it gave livePlugin, which the Vue composables follow (details). A Go client.Client serves one API key.

Documentation

  • docs/wiki: declarations, reads and filters, the live engine, Postgres, REST and metrics, the generated clients, testing, migration from v0.2.x.
  • examples/todo: a service on gin, govengers/auth, sqlc, goose and govengers/jobs, with the gates, the generated clients and the CLI in its tests.
  • docs/design.md: the binding design, decisions D1 onwards.
  • pkg.go.dev: the API.

The repository follows govengers/standards, profile library. just check runs lint, the unit tests, the two-engine end-to-end suite and govulncheck.

License

Apache 2.0, see LICENSE.

Documentation

Overview

Package livewire declares an API once in Go and serves it as REST routes, as a WebSocket with reads, subscriptions and writes, and as generated clients (TypeScript, Go, CLI) plus an OpenAPI document. The registry is the single source of truth; the design is in docs/design.md.

Example (Application)

Example_application declares the README's API and renders its TypeScript client and OpenAPI document; serve wires it on gin and PostgreSQL.

package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/google/uuid"
	"github.com/govengers/auth/authorization"
	authginadapter "github.com/govengers/auth/ginadapter"
	"github.com/govengers/auth/session"
	"github.com/jackc/pgx/v5"
	"github.com/jackc/pgx/v5/pgxpool"

	"github.com/govengers/livewire"
	"github.com/govengers/livewire/generate"
	"github.com/govengers/livewire/ginadapter"
	"github.com/govengers/livewire/postgresadapter"
	"github.com/govengers/livewire/query"
)

// The README's example, compiled with the module: the docs quote the regions between the
// snippet markers, and TestDocumentSnippetsMatchTheirSources fails when a quote drifts.

// todoRepository reads and writes the todos of the README's example on PostgreSQL.
type todoRepository struct {
	pool *pgxpool.Pool
}

// todoSQL is where the todos resource reads its rows from (D11).
var todoSQL = &query.ResourceSQL{From: "todos", IDColumn: "todos.id"}

// List is the resource's loader: the page query, the capped count and the hydration on one
// snapshot, the engine's read transaction (D53, D55).
func (r *todoRepository) List(ctx context.Context, listQuery *query.ListQuery) (*livewire.Page[todo], error) {
	return postgresadapter.ListPage(ctx, r.pool, todoSQL, listQuery, r.hydrate)
}

// Create runs in the mutation's transaction, which the engine owns (D31): it commits with the
// audit row and the idempotency record, or not at all.
func (r *todoRepository) Create(ctx context.Context, _ *livewire.NoParameters, body *createTodo) (*todo, error) {
	database := postgresadapter.TransactionFrom(ctx)
	if database == nil {
		database = r.pool // outside an engine unit of work
	}
	created := &todo{Title: body.Title}
	err := database.QueryRow(ctx, "INSERT INTO todos (id, title) VALUES (coalesce($1, uuidv7()), $2) RETURNING id, done, created_at",
		body.ID, body.Title).Scan(&created.ID, &created.Done, &created.CreatedAt)
	return created, err
}

func (r *todoRepository) hydrate(ctx context.Context, database postgresadapter.DBTX, rows []postgresadapter.PageRow) (map[uuid.UUID]todo, error) {
	result, err := database.Query(ctx, "SELECT id, title, done, created_at FROM todos WHERE id = ANY($1)", postgresadapter.PageIDs(rows))
	if err != nil {
		return nil, err
	}
	defer result.Close()
	found, err := pgx.CollectRows(result, pgx.RowToStructByPos[todo])
	if err != nil {
		return nil, err
	}
	todos := make(map[uuid.UUID]todo, len(found))
	for _, item := range found {
		todos[item.ID] = item
	}
	return todos, nil
}

// snippet:begin declare
func declare(todos *todoRepository) (*livewire.Registry, error) {
	registry, err := livewire.NewRegistry(&livewire.RegistryOptions{BasePath: "/api/v1"})
	if err != nil {
		return nil, err
	}
	return registry, registry.Add( // a declaration mistake fails here, at startup (D43)
		livewire.NewResource("todos", "/todos", &livewire.ResourceOptions[todo]{
			Tables: []string{"todos"}, // a committed write to todos re-runs every live read of it
			Read:   authorization.IsUser(),
			Fields: query.Fields{
				{Name: "title", Type: query.TypeText, SQL: "todos.title", Filter: true, Sort: true, Search: true},
				{Name: "done", Type: query.TypeBoolean, SQL: "todos.done", Filter: true, Sort: true},
				{Name: "createdAt", Type: query.TypeTime, SQL: "todos.created_at", Filter: true, Sort: true},
			},
			DefaultSort: []query.SortTerm{{Field: "createdAt", Descending: true}},
			List:        todos.List,
		}),
		livewire.NewMutation("createTodo", http.MethodPost, "/todos", &livewire.MutationOptions[livewire.NoParameters, createTodo, todo]{
			Check: authorization.IsUser(), Status: http.StatusCreated,
			ClientID: livewire.UUIDv7, Effect: livewire.Upserts("todos"), // with `optimistic`, a socket call shows it at once
			Handle: todos.Create,
		}),
	)
}

// snippet:end declare

func serve(ctx context.Context, pool *pgxpool.Pool, databaseURL string, guard *session.Guard, router gin.IRouter) error {
	registry, err := declare(&todoRepository{pool: pool})
	if err != nil {
		return err
	}
	transactor, err := postgresadapter.NewTransactor(&postgresadapter.TransactorOptions{Pool: pool})
	if err != nil {
		return err
	}
	listener, err := postgresadapter.NewListener(&postgresadapter.ListenerOptions{DSN: databaseURL})
	if err != nil {
		return err
	}
	// snippet:begin serve
	engine, err := livewire.NewEngine(&livewire.EngineOptions{
		Registry: registry, ChangeFeed: listener, Transactor: transactor,
		IdentitySource: guard, // re-validates the session of every socket (D6)
		AllowedOrigins: []string{"https://todo.example.com"},
		// IdempotencyStore, AuditStore, FilterStore, PresenceStore: each feature is off without its store
	})
	if err != nil {
		return err
	}
	api := router.Group("", authginadapter.Middleware(guard.RequireAPI())) // the identity of every request
	if err := ginadapter.RegisterRoutes(api, &ginadapter.Options{Engine: engine}); err != nil {
		return err
	}
	api.GET("/api/v1/socket", ginadapter.SocketHandler(engine))
	err = engine.Run(ctx) // the change feed and the ticks, until ctx ends
	closing, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
	defer cancel()
	return errors.Join(err, engine.Close(closing)) // every socket ends with 1001
	// snippet:end serve
}

// Example_application declares the README's API and renders its TypeScript client and
// OpenAPI document; serve wires it on gin and PostgreSQL.
func main() {
	registry, err := declare(&todoRepository{})
	if err != nil {
		fmt.Println(err)
		return
	}
	_ = serve // main calls it with its pool, session guard and router
	// snippet:begin generate
	options := &generate.Options{
		TypeScriptDirectory: "web/src/api", // types.ts, filters.ts, api.ts, runtime.ts, vue.ts
		OpenAPIFile:         "openapi.yaml",
		OpenAPI:             generate.OpenAPIOptions{Title: "Todo", Version: "1.0.0"},
	}
	// snippet:end generate
	files, err := generate.Render(registry, options) // what generate.Write(registry, options) writes
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, file := range files {
		fmt.Println(file.Path)
	}
}
Output:
web/src/api/types.ts
web/src/api/filters.ts
web/src/api/api.ts
web/src/api/runtime.ts
web/src/api/vue.ts
openapi.yaml

Index

Examples

Constants

View Source
const (
	BatchPath   = "/batch"   // POST: a batch of mutations (D33, D47)
	FiltersPath = "/filters" // POST stores, GET lists the caller's stored filters; DELETE FiltersPath/{id} removes one (D13)
)

The routes the library serves itself, below the registry's BasePath; no declaration may take them (Registry.Add refuses one that does).

View Source
const (
	BatchTarget   = "batch"
	FiltersTarget = "filters"
)

The targets of the outcomes of the library's own routes (D59); both are reserved names. BatchTarget is also the prefix of the name a batch's Idempotency-Key is reserved under.

View Source
const (
	ParameterFilter   = "filter"   // an inline filter document (JSON)
	ParameterFilterID = "filterId" // a stored filter's id; the engine resolves it to the document
	ParameterSort     = "sort"     // comma-separated fields, "-" prefix for descending
	ParameterLimit    = "limit"
	ParameterOffset   = "offset"
	ParameterAfter    = "after" // the cursor of the previous page (cursor mode)
	ParameterField    = "field" // the field of a distinct read
)

The reserved parameters of a filterable resource's reads (D11, D13, D15, D38). The core handles them; a declaration's own parameters never use these names.

View Source
const (
	DefaultMaximumLimit    = 100
	DefaultDistinctMaximum = 1000
)

Default limits of a filterable resource.

View Source
const (
	CloseNormal       = 1000 // the client ended the socket
	CloseGoingAway    = 1001 // Engine.Close ends every socket with it (section 5)
	CloseTooSlow      = 1013 // the client fell behind the per-socket queue limit (D10)
	CloseIdentityGone = 4401 // the identity is gone: no session, expired, revoked (D7)
)

The close codes of a socket.

View Source
const ExportCSV = "csv"

ExportCSV is the format of a CSV export.

View Source
const ParameterFormat = "format"

ParameterFormat is the parameter of an export that names its format (D35); ExportCSV, the only one the core serves, is the default.

View Source
const ParameterPresenceKey = "key"

ParameterPresenceKey is the parameter of a presence read, join and leave that names the record (D37): the item key of a resource, any key within an object.

View Source
const PresenceTable = "livewire_presence"

PresenceTable is the table whose changes re-run presence subscriptions; the postgresadapter's presence trigger notifies on it (D37).

Variables

This section is empty.

Functions

func HistoryFields

func HistoryFields() query.Fields

HistoryFields are the fields a history read filters and sorts by (D34), named as Version marshals them and written over the columns of `<table>_history`: the loader's FROM is the history table alone. The default sort is by version, oldest first. A live item's history may hold delete versions: a partition move writes a delete and an insert (D56).

func IdempotencyKeyFrom

func IdempotencyKeyFrom(ctx context.Context) string

IdempotencyKeyFrom returns the Idempotency-Key of the mutation in ctx, or "" outside one.

func OwnerKey

func OwnerKey(identity *auth.Identity) string

OwnerKey is the key an identity owns rows, sockets and stored filters under: "provider:subject". Engine.RevalidateOwner and SocketsPerOwner take the same key; a nil identity has the key "".

func RedactItem

func RedactItem(ctx context.Context, fields query.Fields, raw json.RawMessage) (json.RawMessage, error)

RedactItem is RedactItems for one marshaled object: unchecked when nothing is guarded for the identity, otherwise anything but an object is an error.

func RedactItems

func RedactItems(ctx context.Context, fields query.Fields, raw json.RawMessage) (json.RawMessage, error)

RedactItems strips the fields whose Read check refuses the identity in ctx from every item of raw, a marshaled JSON array of objects (D39). Fields that are visible or absent are left alone. When no field is guarded for the identity, raw is returned as it is and unchecked; otherwise the result is re-marshaled with its keys sorted and anything but an array of objects is an error, so a wrong caller fails instead of leaking.

func TenantFrom

func TenantFrom(ctx context.Context) string

TenantFrom returns the tenant of the request in ctx, "" in a single-tenant registry or outside a request.

func TransactionFrom

func TransactionFrom(ctx context.Context) any

TransactionFrom returns the query-only handle of the engine transaction in ctx, or nil outside an engine unit of work, so a repository falls back to its pool. Its type is the adapter's: postgresadapter.TransactionFrom(ctx) types it as sqlc's DBTX (D45).

func WithIdempotencyKey

func WithIdempotencyKey(ctx context.Context, key string) context.Context

WithIdempotencyKey returns a copy of ctx carrying the Idempotency-Key of the call.

func WithTenant

func WithTenant(ctx context.Context, tenant string) context.Context

WithTenant returns a copy of ctx carrying the tenant the registry's Tenant function resolved (D42); the engine calls it once per request or subscription after the static check.

func WithTransaction

func WithTransaction(ctx context.Context, handle any) context.Context

WithTransaction returns a copy of ctx carrying the query-only handle of the engine's transaction (Transaction.Handle); the engine calls it for every unit of work, reads included (D45), before a loader or handler runs. Only the engine holds the Transaction itself, so nothing in the context can commit or roll back.

Types

type AuditEntry

type AuditEntry struct {
	Tenant         string
	Identity       *auth.Identity
	Mutation       string
	Parameters     any
	RequestID      string
	IdempotencyKey string
	Status         int
	Duration       time.Duration
}

AuditEntry is one mutation call as the action audit of D32 records it: the tenant, the effective identity (its Real() is the real actor), the mutation, its decoded parameters, the request id, the Idempotency-Key ("" for none), the answered status and how long the call took.

type AuditStore

type AuditStore interface {
	Write(ctx context.Context, entry *AuditEntry) error
}

AuditStore keeps the action audit of D32. The engine writes one entry per call whose handler ran: a mutation, an upload and every call of a batch. For a call that commits it writes the entry in the mutation's transaction (TransactionFrom(ctx)) between the handler and the idempotency record, so the entry commits and rolls back with the call (section 7); a failing Write fails the call. For a call that failed it writes the entry after the rollback, without a transaction in ctx and on a context the caller's cancellation does not reach (bounded), so the store writes it on its own: the failed calls of a batch each with the status the batch answered. A refusal before the handler (a check, a decoding, a replayed or in-flight key) writes none.

type BatchCall

type BatchCall struct {
	Target     string            `json:"target"`
	Parameters map[string]string `json:"params,omitempty"`
	Body       json.RawMessage   `json:"body,omitempty"`
}

BatchCall is one call of a batch: a mutation's name, its parameters as a socket frame carries them, and its body.

type BatchRequest

type BatchRequest struct {
	Body           json.RawMessage
	IdempotencyKey string
	RequestID      string
}

BatchRequest is one batch (D33) as a transport hands it to Engine.Batch: the body, a JSON array of BatchCall, the batch's one Idempotency-Key and the request id.

type BatchResult

type BatchResult struct {
	Status  int             `json:"status"`
	Payload json.RawMessage `json:"payload,omitempty"`
}

BatchResult is the answer of one call of a committed batch, in the order of the calls: the status the call answers on its own and its body (absent for NoContent).

type CallDescriber

type CallDescriber interface {
	DescribeCall(ctx context.Context, transactionDetails TransactionDetails) error
}

CallDescriber is the optional interface of a Transaction that runs several calls (D58): the engine calls DescribeCall before each call of a batch (D33) with that call's details, so the triggers attribute each call's writes to its own mutation and request id (D34); the postgresadapter sets livewire.actor (actor, real actor, mutation, request id) again (SET LOCAL). The call's parameters reach its audit entry only. Without it a batch's history rows carry the outer transaction's details, whose Mutation is empty. A failing DescribeCall fails the batch.

type CallRequest

type CallRequest struct {
	Target         string
	Source         ParameterSource
	Body           []byte
	IdempotencyKey string
	RequestID      string
}

CallRequest is one mutation call as a transport hands it to Engine.Call: the mutation's name, the request's parameters, the raw JSON body (empty for none), the Idempotency-Key (D16, empty for none) and the request id the transaction records (D31; a new one when empty, which is what the socket does: frame ids are the client's and repeat).

type Change

type Change struct {
	Table     string
	Operation ChangeOperation
	ID        string
	Partition string
}

Change is one row change a ChangeFeed reports: the table, the operation, the row's id as text and, for partitioned triggers, the value of the partition column (D25); Partition is empty otherwise.

type ChangeFeed

type ChangeFeed interface {
	Run(ctx context.Context, onChange func(Change), onResync func()) error
}

ChangeFeed delivers row changes to the engine. Run blocks until ctx ends and returns ctx.Err() then, or the failure that ended the feed for good; it reconnects on its own in between. It calls onResync after every successful connect, the first included, because changes may have been missed, and onChange once per change; both are called from the feed's goroutine and must return quickly.

type ChangeOperation

type ChangeOperation string

ChangeOperation is what happened to a row.

const (
	ChangeInsert ChangeOperation = "insert"
	ChangeUpdate ChangeOperation = "update"
	ChangeDelete ChangeOperation = "delete"
)

The change operations a trigger reports.

type ClientIDVersion

type ClientIDVersion int

ClientIDVersion says which client-generated `id` a create accepts (D18).

const (
	ClientIDNone ClientIDVersion = iota
	UUIDv7
	UUIDv4
)

The client id versions. Without one (the zero value) a create accepts no `id` in the body.

type Code

type Code string

Code is the stable client-facing code of an error body. The set is closed: generated clients switch on it, so a custom code of an *auth.Error is mapped to its kind's code.

const (
	CodeInvalidRequest  Code = "invalid_request"
	CodeInvalidFilter   Code = Code(query.CodeInvalidFilter)
	CodeInvalidSort     Code = Code(query.CodeInvalidSort)
	CodeInvalidPage     Code = Code(query.CodeInvalidPage)
	CodeInvalidCursor   Code = Code(query.CodeInvalidCursor)
	CodeForbiddenField  Code = "forbidden_field"
	CodeUnauthenticated Code = "unauthenticated"
	CodeForbidden       Code = "forbidden"
	CodeNotFound        Code = "not_found"
	CodeConflict        Code = "conflict"
	CodeInProgress      Code = "in_progress"
	CodeInternal        Code = "internal"
)

The error codes. The invalid_* codes of the query package are repeated here so the set is complete in one place; ResponseFor maps a *query.Error to its code.

type Declaration

type Declaration interface {
	Kind() Kind
	Name() string
	Method() string
	Path() string
	Tables() []string
	Check() authorization.Check
	Parameters() []Parameter
	DecodeParameters(source ParameterSource) (any, error)
	// contains filtered or unexported methods
}

Declaration is one entry of the registry: a *Resource, *List, *Object, *Mutation or *Upload. The interface is sealed to those five kinds; a type that embeds one of them is refused by Registry.Add. Path is relative to the registry's BasePath and uses `{name}` placeholders. Name is the wire target and the identifier of the generated clients; Method is the HTTP method of the main REST route (GET for reads); Tables are the tables whose changes re-run the declaration's subscriptions, none means not live; Check is the static gate (D8) that runs before parameters are resolved against the database and before a body is read; Parameters describes the parameters struct for the generators; DecodeParameters fills a new parameters value (a *Parameters) from source and answers a missing path parameter, a repeated scalar parameter or a value of the wrong form with an *Error of 400 invalid_request naming the parameter.

type DistinctValue

type DistinctValue struct {
	Value any `json:"value"`
	Count int `json:"count"`
}

DistinctValue is one row of a distinct read (D15): a value of the field and how many rows carry it.

type Effect

type Effect struct {
	Kind   EffectKind
	Target string
}

Effect is the optimistic effect of a mutation on the live lists of Target (D36); the zero value is no effect.

func Removes

func Removes(target string) Effect

Removes is the effect that removes the item named by the `id` path parameter from the live lists of target; the mutation's parameters must carry `path:"id"`.

func Upserts

func Upserts(target string) Effect

Upserts is the effect that upserts the response into the live lists of target; the response type must carry a JSON field "id".

type EffectKind

type EffectKind string

EffectKind is what an optimistic effect does to a live list (D36).

const (
	EffectUpserts EffectKind = "upserts" // the response is upserted into the list
	EffectRemoves EffectKind = "removes" // the item whose id is the request's `id` path parameter is removed
)

The effect kinds.

type Engine

type Engine struct {
	// contains filtered or unexported fields
}

Engine serves an API's socket (section 4) and runs the read and write pipelines both transports share (D23): Read, Call, Batch and Upload are what the REST adapter calls, the socket ops run the same code. Build it with NewEngine, serve Handler, run Run, and end it with Close. It is safe for concurrent use.

func NewEngine

func NewEngine(options *EngineOptions) (*Engine, error)

NewEngine validates and copies options; the caller's struct is never written and its AllowedOrigins slice is copied.

Example

ExampleNewEngine wires the engine in main: the registry, the IdentitySource that re-validates every socket (session.Guard), the Transactor and the change feed of postgresadapter, the browser origins that may open a socket. Handler serves the socket behind the same authentication as the REST routes; Run consumes the change feed until the context ends; Close ends every socket with 1001.

package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/govengers/livewire"
)

func main() {
	var (
		ctx            context.Context
		registry       *livewire.Registry      // built with NewRegistry and Add
		identitySource livewire.IdentitySource // session.Guard
		transactor     livewire.Transactor     // postgresadapter
		changeFeed     livewire.ChangeFeed     // postgresadapter's LISTEN feed
		authenticate   func(http.Handler) http.Handler
	)
	engine, err := livewire.NewEngine(&livewire.EngineOptions{
		Registry:       registry,
		ChangeFeed:     changeFeed,
		IdentitySource: identitySource,
		Transactor:     transactor,
		AllowedOrigins: []string{"https://app.example"},
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	http.Handle("/api/v1/socket", authenticate(engine.Handler())) // the Go client's default socket URL
	go func() { _ = engine.Run(ctx) }()
	defer func() { _ = engine.Close(ctx) }()
}

func (*Engine) Batch

func (e *Engine) Batch(ctx context.Context, request BatchRequest) (json.RawMessage, error)

Batch runs several mutations in one engine transaction, all or nothing (D33): every call is checked and decoded first (a refusal answers before any transaction begins), the calls must share one tenant, the batch's Idempotency-Key is reserved once, the handlers run in order, and the first failure rolls everything back. The payload is a JSON array of BatchResult. A refusal names the failing call: its fields are prefixed with "calls.<index>." and "calls.<index>" carries its code. Uploads are excluded. Batch reports an OutcomeCall per call that ran, an OutcomeBatch, and an OutcomeAfterCommit per call after the commit.

func (*Engine) Call

func (e *Engine) Call(ctx context.Context, request CallRequest) (status int, payload json.RawMessage, err error)

Call runs one mutation for the identity in ctx (section 7), the pipeline the socket's call op runs too: the static Check (D8), the parameters, the tenant (D42), the body (unknown fields refused, D40; field write checks, D39; validation, D44), then the engine transaction (D31): the Idempotency-Key reserved through the IdempotencyStore (a finished key answers its recorded response, one in flight 409 in_progress, D16), the handler, the idempotency record, the commit. It answers the mutation's status and the marshaled response (nil for NoContent). An upload is REST only and refused here. Call reports an OutcomeCall and, after the commit, an OutcomeAfterCommit (D41); a replay is not committed again and reports none.

func (*Engine) Close

func (e *Engine) Close(ctx context.Context) error

Close ends every socket with CloseGoingAway, stops Run, refuses new sockets with 503 and waits until every socket's goroutines and Run ended or ctx is done, which it returns then; a close frame a client does not take is given up at ctx's deadline at the latest. Close may be called more than once.

func (*Engine) DeleteFilter

func (e *Engine) DeleteFilter(ctx context.Context, id string) error

DeleteFilter removes the caller's stored filter (DELETE /filters/{id}, D13); a filter that is not there, or another owner's, is no error. Without a FilterStore it is 404, without an identity 401.

func (*Engine) Export

func (e *Engine) Export(ctx context.Context, request ExportRequest) (rows int, err error)

Export streams every row of a filterable resource under the identity in ctx (D35), REST only: authorized and resolved like a list read (the static Check, the reserved parameters with a stored filter resolved, the tenant), the format checked (csv, the default; another is 400 at "format"), then in a read-only engine transaction the scope, the filter and sort compiled like a list read's and the Export loader run with the export columns whose Read check passes (D39) and request.Writer. Nothing is written before the loader writes, so a refusal or a failure before it can still be answered as an error body. It returns the rows the loader wrote and reports an OutcomeExport carrying them.

func (*Engine) Handler

func (e *Engine) Handler() http.Handler

Handler is the socket endpoint: it refuses an Origin that is neither allowed nor the request's own host with 403 (D9; a request without Origin is a browser's only when it carries cookies, so it is refused then and admitted otherwise), a request without an identity in its context (auth.IdentityFrom; put the handler behind the same authentication as the REST routes) with 401, an owner past SocketsPerOwner with 429 and a closing engine with 503, each with the one error body; then it upgrades and serves the socket until it ends.

func (*Engine) ListFilters

func (e *Engine) ListFilters(ctx context.Context, resource string) (json.RawMessage, error)

ListFilters answers the caller's named filters of a resource (or "name.history"), of every one when resource is "" (GET /filters, D13), as a ListResponse of FilterItem; unnamed filters are registrations and not listed. Every filter is authorized again under the caller as SaveFilter does: one whose resource's static Check refuses the caller now, or whose document names a field the caller may no longer read (D39), is left out. A resource that is not a registered filterable resource is 400; without a FilterStore it is 404, without an identity 401.

func (*Engine) Read

func (e *Engine) Read(ctx context.Context, request ReadRequest) (json.RawMessage, error)

Read runs one read for the identity in ctx, the pipeline the socket's query, subscribe and re-runs run too (D23): the target's static Check (D8), the parameters (a resource's reserved ones, a stored filter resolved through the FilterStore, D13), the tenant (D42), then in a read-only engine transaction (D45) the scope, the loader and the field redaction (D39). A presence read first runs the record's own read under the caller (a resource's item read for the key, an object's read), so Scope and ownership decide who sees who is there. The payload is the REST body (a ListResponse for lists, D14); it is answered with 200. A refusal or failure is the error, answered through ResponseFor. Read reports an OutcomeRequest to the registry's Observer.

func (*Engine) Registry

func (e *Engine) Registry() *Registry

Registry is the registry the engine serves.

func (*Engine) RevalidateAll

func (e *Engine) RevalidateAll()

RevalidateAll re-validates the identity of every socket now, through the IdentitySource (section 6, push-driven revocation): a gone identity closes its socket 4401, a changed one re-runs its subscriptions. It returns at once; the checks run on each socket's own goroutine.

func (*Engine) RevalidateOwner

func (e *Engine) RevalidateOwner(key string)

RevalidateOwner is RevalidateAll for the sockets whose effective or real identity has the owner key (OwnerKey).

func (*Engine) Run

func (e *Engine) Run(ctx context.Context) error

Run consumes the change feed and drives the ticks until ctx ends or Close is called, and returns nil then. Every successful connect of the feed, the first included, re-runs every subscription with a snapshot (section 5); a feed that fails is restarted after a backoff with jitter, from RetryInterval doubling up to 32 times it, reset by a successful connect, and reported as an OutcomeFeed. The ticks re-validate every socket's identity (RevalidateInterval), re-run the time-dependent subscriptions (SafetyTickInterval), and refresh and sweep presences (PresenceHeartbeat, PresenceExpiry). Run returns an error when it is already running; after Close it returns nil at once. Close waits for it.

func (*Engine) SaveFilter

func (e *Engine) SaveFilter(ctx context.Context, body []byte) (json.RawMessage, error)

SaveFilter stores a filter for the caller (POST /filters, D13): body is a FilterRequest (unknown fields refused); the resource must be a registered filterable resource, or "name.history" for its history read over HistoryFields (D56), whose static Check passes, the document must parse against its fields and name only fields the caller may read (D39). The filter is the owner's under the tenant of a resource read (D42). The payload is the stored FilterItem, answered with 201; an unnamed filter the owner stored already for the resource and context is that filter. Without a FilterStore it is 404, without an identity 401. It reports an OutcomeRequest with the target "filters".

func (*Engine) ServesPresence

func (e *Engine) ServesPresence() bool

ServesPresence reports whether the engine has a PresenceStore, so presence reads and the join and leave ops are on (D37).

func (*Engine) Upload

func (e *Engine) Upload(ctx context.Context, request UploadRequest) (status int, payload json.RawMessage, err error)

Upload runs one upload for the identity in ctx (D17), REST only: the static Check, the parameters, the tenant, the file against the declared content types and size, then the engine transaction with the Idempotency-Key as Call has it. It answers the upload's status and the marshaled response, and reports like Call.

type EngineOptions

type EngineOptions struct {
	Registry               *Registry
	ChangeFeed             ChangeFeed
	IdentitySource         IdentitySource
	Transactor             Transactor
	FilterStore            FilterStore
	IdempotencyStore       IdempotencyStore
	AuditStore             AuditStore
	PresenceStore          PresenceStore
	AllowedOrigins         []string
	SameHostOnly           bool
	SubscriptionsPerSocket int
	InFlightPerSocket      int
	PresencesPerSocket     int
	FiltersPerSocket       int
	QueuedBytesPerSocket   int
	FrameBytes             int64
	QueryConcurrency       int
	SocketsPerOwner        int
	RevalidateInterval     time.Duration
	SafetyTickInterval     time.Duration
	PresenceHeartbeat      time.Duration
	PresenceExpiry         time.Duration
	DeliveryBatchWait      time.Duration
	RetryInterval          time.Duration
	WriteTimeout           time.Duration
}

EngineOptions configures the live engine (section 5). Registry, IdentitySource and Transactor are required; AllowedOrigins is required unless SameHostOnly says that only the request's own host may open a socket (D9). ChangeFeed is optional: without one, a subscription re-runs only when its socket's identity changes and, when its filter carries a relative time, on the safety tick. FilterStore enables stored filters (D13), IdempotencyStore the replay of mutations (D16), AuditStore the action audit (D32), PresenceStore the join and leave ops and the presence reads (D37); each is optional and its feature is off without it. The engine's outcomes go to the registry's Observer (D49). The engine runs no store maintenance: the sweeps and the audit partitions (the postgresadapter's FilterStore.Sweep, IdempotencyStore.Sweep and AuditStore.Maintain) are the application's to schedule, a periodic govengers/jobs job for instance, so one replica runs them, not every engine, and a failure lands where the application handles its jobs.

The limits (D10) default when zero: SubscriptionsPerSocket 64, InFlightPerSocket 16, PresencesPerSocket 64 (records one socket may join at once), FiltersPerSocket 64 (the distinct inline filters the live subscriptions of one socket hold, D13; it bounds no filter rows beyond that socket's live ones), QueuedBytesPerSocket 8 MiB, FrameBytes 64 KiB, QueryConcurrency 32 (D52); SocketsPerOwner 0 is unlimited. The intervals default when zero: RevalidateInterval 60 s (D6, the cache of a socket's identity for re-runs and the tick that re-validates every socket), SafetyTickInterval 30 s (the re-run of time-dependent subscriptions), PresenceHeartbeat 30 s and PresenceExpiry 90 s (D37), DeliveryBatchWait 100 ms (the longest a push waits after its first change, so a burst leaves as one push per subscription), RetryInterval 1 s (the first backoff of a failed re-run or change feed, doubling up to 32 times it) and WriteTimeout 10 s (a write the client does not take in time closes its socket 1013). No value may be negative; PresenceExpiry must exceed PresenceHeartbeat.

type Error

type Error struct {
	// contains filtered or unexported fields
}

Error is a refusal the core or a handler answers with its status and body. Only the constructors below build one, so the code is from the closed set and the message is the code's fixed text. Match it with errors.As.

func Conflict

func Conflict() *Error

Conflict is the 409 of a write that contradicts the current state (a duplicate, a stale version).

func ForbiddenField

func ForbiddenField(name string) *Error

ForbiddenField is the 403 of a filter, sort, search, distinct or body write on a field whose column check refuses the identity (D39).

func InProgress

func InProgress() *Error

InProgress is the 409 of a replayed idempotency key whose first call has not finished (D16).

func Invalid

func Invalid(fields map[string]string) *Error

Invalid is the 400 of a request whose parameters or body are wrong; fields maps each offending name to what is wrong with it and is copied.

func NotFound

func NotFound() *Error

NotFound is the 404 a loader or handler returns when the addressed item does not exist or the identity may not learn that it does.

func (*Error) Body

func (e *Error) Body() ErrorBody

Body is the error body; the result is the caller's copy.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Status

func (e *Error) Status() int

Status is the HTTP status of the refusal.

type ErrorBody

type ErrorBody struct {
	Code    Code              `json:"code"`
	Message string            `json:"message"`
	Fields  map[string]string `json:"fields,omitempty"`
}

ErrorBody is the one JSON error shape of every REST response and socket error. Message is fixed per code and safe to show; Fields names the offending parameters or body fields (nested ones as a dotted path) with a fixed text per fault.

func ResponseFor

func ResponseFor(err error) (int, ErrorBody)

ResponseFor maps any error to the status and body a client gets. The outermost error of a known kind in the chain decides, so a refusal that wraps a cause answers as the refusal: a *Error answers as it is; a *query.Error answers 400 with its code and fields; an *auth.Error or a bare auth sentinel answers its kind's status with the kind's code from the closed set (a custom auth code is dropped); anything else, a nil *Error included, is a 500 with the code internal and no text of the cause.

type ExportRequest

type ExportRequest struct {
	Target string
	Source ParameterSource
	Writer io.Writer
}

ExportRequest is one export (D35) as the REST adapter hands it to Engine.Export: the resource's name, the request's parameters (the reserved ones of a list read and `format`) and the writer the export streams to.

type File

type File struct {
	ContentType string
	Size        int64
	Body        io.Reader
	Filename    string
}

File is the binary body of an upload as the handler receives it: the declared content type it matched (without parameters), its size in bytes, the body to read once, and the client's file name when a multipart part carried one.

type FilterItem

type FilterItem struct {
	ID         string          `json:"id"`
	Resource   string          `json:"resource"`
	Context    string          `json:"context,omitempty"`
	Name       string          `json:"name,omitempty"`
	Filter     json.RawMessage `json:"filter"`
	CreatedAt  time.Time       `json:"createdAt"`
	LastUsedAt time.Time       `json:"lastUsedAt"`
}

FilterItem is a stored filter as the filter routes answer it: Filter is the canonical document a filterId read applies.

type FilterRequest

type FilterRequest struct {
	Resource string          `json:"resource"`
	Context  string          `json:"context,omitempty"`
	Name     string          `json:"name,omitempty"`
	Filter   json.RawMessage `json:"filter"`
}

FilterRequest is the body of POST /filters (D13): the resource the filter is for (its name, or "name.history" for its history read, D56), the application's context of the dedup key ("" for none), a name ("" for an unnamed filter, which is swept after 30 days without use) and the filter document.

type FilterStore

type FilterStore interface {
	// Save stores filter and returns the stored one with its ID; an unnamed filter whose
	// Hash the owner already stored for the resource and context returns that filter.
	Save(ctx context.Context, filter *StoredFilter) (*StoredFilter, error)
	// Get returns the owner's filter and marks it used; it returns NotFound when there is
	// none.
	Get(ctx context.Context, owner Owner, id string) (*StoredFilter, error)
	// List returns the owner's named filters of a resource, every resource when resource is
	// empty; unnamed filters (inline subscribe registrations) are not listed.
	List(ctx context.Context, owner Owner, resource string) ([]StoredFilter, error)
	// Delete removes the owner's filter; a filter that is not there is no error.
	Delete(ctx context.Context, owner Owner, id string) error
}

FilterStore keeps the stored filters of D13. Filters are visible to their owner only, the tenant included: every method takes the owner and treats another owner's filter as absent.

type GuardedField

type GuardedField struct {
	Name string
	Read authorization.Check
}

GuardedField names a field of a typed-parameters list or object whose Read check guards it (D39): a refused check strips the field from every item.

type IdempotencyRecord

type IdempotencyRecord struct {
	Owner    Owner
	Key      string
	Mutation string
	Status   int
	Response json.RawMessage
}

IdempotencyRecord is the recorded answer of a mutation call (D16): the owner, the key the client sent, the mutation it was sent for, and the status and body that were answered. Status is 0 while the call is in flight.

type IdempotencyStore

type IdempotencyStore interface {
	// Reserve claims the key for the owner and mutation. A fresh key returns replay false;
	// a key whose call finished returns the record and replay true; a key whose call is
	// still in flight returns InProgress; a key recorded for another mutation returns
	// Conflict.
	Reserve(ctx context.Context, owner Owner, key, mutation string) (record *IdempotencyRecord, replay bool, err error)
	// Record stores the answer of a reserved key.
	Record(ctx context.Context, record *IdempotencyRecord) error
}

IdempotencyStore keeps the records of D16. Both methods run inside the mutation's transaction (TransactionFrom(ctx)), so a rolled-back call frees its key. Reserve must not block on another transaction's reservation of the same key: the postgresadapter takes pg_try_advisory_xact_lock over a hash of the owner and key, answers InProgress when the lock is held, replays a committed record, and inserts the reservation otherwise.

type IdentitySource

type IdentitySource interface {
	Identify(ctx context.Context, request *http.Request) (*auth.Identity, error)
}

IdentitySource re-validates who a socket acts as (D6): the engine calls Identify with the upgrade request on every subscribe, query, call, batch, join and leave, on a re-run whose cached identity is older than RevalidateInterval, and on that tick (D51; ping and unsubscribe are exempt). The identity is gone, and the socket closes with CloseIdentityGone, when the source returns an error matching auth.ErrUnauthenticated (no session, expired, revoked: what session.Guard.Identify returns) or auth.ErrForbidden (a refused refresh), or a nil identity with a nil error. Any other error is a failure of the source: the engine keeps the last identity and retries.

type ItemKey

type ItemKey struct {
	Parameter string
	Field     string
}

ItemKey says how a resource's item read addresses one row: the path parameter of the item route (`{Parameter}` below the resource path) and the declared field it matches, which must be filterable and of type uuid, string, text, enum, int or time. On REST the resource's own routes `distinct`, `export` and `presence` sit beside the item route and win, so an item whose key is one of these words is readable on the socket only; a key of type uuid never is.

type Kind

type Kind string

Kind is the kind of a declaration.

const (
	KindResource Kind = "resource" // NewResource: a filterable list with item and distinct reads
	KindList     Kind = "list"     // NewList: a typed-parameters list
	KindObject   Kind = "object"   // NewObject: a typed-parameters object
	KindMutation Kind = "mutation" // NewMutation: a write with a JSON body
	KindUpload   Kind = "upload"   // NewUpload: a binary write, REST only
)

The declaration kinds.

type List

type List struct {
	// contains filtered or unexported fields
}

List is a read that answers a ListResponse of every item its loader returns for the typed parameters; it has no filter, sort or page of its own.

func NewList

func NewList[Item, Parameters any](name, path string, options *ListOptions[Item, Parameters]) *List

NewList declares a typed-parameters list of Item at path, whose `{name}` placeholders are the `path:"name"` fields of Parameters. The options are copied. A mistake is reported by Registry.Add (D43).

func (*List) Check

func (s *List) Check() authorization.Check

func (*List) DecodeParameters

func (s *List) DecodeParameters(source ParameterSource) (any, error)

func (*List) Fields

func (l *List) Fields() query.Fields

Fields are the guarded fields (name and Read check only) for RedactItems; the result is the caller's copy.

func (*List) ItemType

func (l *List) ItemType() reflect.Type

ItemType is the Go type of one item.

func (*List) Kind

func (l *List) Kind() Kind

Kind is KindList.

func (*List) Load

func (l *List) Load(ctx context.Context, parameters any) (any, error)

Load runs the loader with the decoded parameters (a *Parameters); the items are a []Item, never nil.

func (*List) Method

func (s *List) Method() string

func (*List) Name

func (s *List) Name() string

func (*List) Parameters

func (s *List) Parameters() []Parameter

func (*List) Partition

func (l *List) Partition() PartitionResolver

Partition is the partition resolver of the declaration (D25), nil without one.

func (*List) Path

func (s *List) Path() string

func (*List) Tables

func (s *List) Tables() []string

type ListChange

type ListChange struct {
	Upserts []json.RawMessage `json:"upserts"`
	Removes []string          `json:"removes"`
	Order   []string          `json:"order,omitempty"`
	Total   *int              `json:"total,omitempty"`
}

ListChange is a list change (the wire's "diff"): replace or insert the upserted items by id, drop the removed ids, re-sort to Order when it is present (the full id list, sent when membership or order changed) and take Total when it is present (D14). Upserts and Removes are never null.

type ListOptions

type ListOptions[Item, Parameters any] struct {
	Tables    []string
	Read      authorization.Check
	Partition PartitionResolver
	Guarded   []GuardedField
	Load      func(ctx context.Context, parameters *Parameters) ([]Item, error)
}

ListOptions declares a typed-parameters list. Read and Load are required; Guarded names the fields of Item whose Read check strips them (D39); a live list (Tables set) needs an item with a JSON field "id". Partition names a subscription's partition (D25); nil means the tenant.

type ListResponse

type ListResponse[Item any] struct {
	Limit  int     `json:"limit"`
	Offset int     `json:"offset"`
	Total  int     `json:"total"`
	Items  []Item  `json:"items"`
	Next   *string `json:"next,omitempty"`
}

ListResponse is the body of every list read (D14): the page that was applied, the total under scope and filter (at most CountCap + 1 when the resource caps it), the items, and in cursor mode the cursor of the next page or nil at the end (D38). Items is never null.

type ListState

type ListState struct {
	// contains filtered or unexported fields
}

ListState is what a list subscription last pushed (the engine keeps one per list subscription; a client applies ListChange the same way): every item's JSON by id, the order of the ids and the total of the list.

func NewListState

func NewListState(rawItems json.RawMessage, total int) (*ListState, error)

NewListState indexes the marshaled items (a JSON array) by their "id", a string or a number. An item without an id, with an id of another shape, or with the id of an earlier item (the number 1 and the string "1" are the same id) is an error: such a list cannot be diffed and must not be pushed.

func (*ListState) ChangesSince

func (s *ListState) ChangesSince(previous *ListState) *ListChange

ChangesSince returns what changed from previous to s: the items that are new or whose JSON differs, the ids that are gone, the full order when membership or order changed, and the total when it changed. It returns nil when nothing changed.

func (*ListState) Snapshot

func (s *ListState) Snapshot() json.RawMessage

Snapshot is the items as one JSON array, in order.

type MapSource

type MapSource map[string]string

MapSource is a ParameterSource over one value per name, as a socket frame carries them: path and query parameters read the same map, a list parameter is one comma-separated value.

func (MapSource) Path

func (s MapSource) Path(name string) (string, bool)

Path returns the value under name.

func (MapSource) Query

func (s MapSource) Query(name string) ([]string, bool)

Query returns the value under name as a one-element list.

type Mutation

type Mutation struct {
	// contains filtered or unexported fields
}

Mutation is a write: a JSON body decoded with unknown fields refused (D40), checked field by field (D39), validated (D44), handled in the engine's transaction (D31).

func NewMutation

func NewMutation[Parameters, Request, Response any](name, method, path string, options *MutationOptions[Parameters, Request, Response]) *Mutation

NewMutation declares a write with method (POST, PUT, PATCH or DELETE) at path, whose `{name}` placeholders are the `path:"name"` fields of Parameters. Request is the body type (a struct, or NoBody), Response the response type (NoContent for none). The options are copied. A mistake is reported by Registry.Add (D43).

func (*Mutation) Check

func (s *Mutation) Check() authorization.Check

func (*Mutation) ClientID

func (m *Mutation) ClientID() ClientIDVersion

ClientID is the accepted client id version (D18).

func (*Mutation) DecodeBody

func (m *Mutation) DecodeBody(ctx context.Context, raw []byte) (any, error)

DecodeBody decodes raw into a new body value (a *Request) under the identity in ctx. The body is read once as a token stream alongside the request type: a body that is not a JSON object, an unknown member at any depth (struct fields, array elements and map values included) and a member that repeats within one object are 400 invalid_request naming the member by its path (D40); a client id a create does not accept, or of the wrong form or version, is 400 naming "id" (D18); a field whose write check refuses is 403 forbidden_field (D39); a body that fails its `validate` tags is 400 naming each field with the failed tag (D44); a value of the wrong type is 400 naming it by the same path. Every text in the response is fixed; the cause of a decoding failure stays on the server. A validator fault the declaration's preflight cannot reach is answered as an internal error, never as a panic. A mutation without a body refuses a non-empty raw.

func (*Mutation) DecodeParameters

func (s *Mutation) DecodeParameters(source ParameterSource) (any, error)

func (*Mutation) Effect

func (m *Mutation) Effect() Effect

Effect is the optimistic effect (D36); the zero value is none.

func (*Mutation) Handle

func (m *Mutation) Handle(ctx context.Context, parameters, body any) (any, error)

Handle runs the handler with the decoded parameters (a *Parameters) and body (a *Request); the response is a *Response.

func (*Mutation) HasBody

func (m *Mutation) HasBody() bool

HasBody reports whether the mutation reads a body.

func (*Mutation) HasContent

func (m *Mutation) HasContent() bool

HasContent reports whether the mutation answers with a body.

func (*Mutation) Kind

func (m *Mutation) Kind() Kind

Kind is KindMutation.

func (*Mutation) Method

func (s *Mutation) Method() string

func (*Mutation) Name

func (s *Mutation) Name() string

func (*Mutation) Parameters

func (s *Mutation) Parameters() []Parameter

func (*Mutation) ParametersType

func (m *Mutation) ParametersType() reflect.Type

ParametersType is the Go type of the parameters struct.

func (*Mutation) Path

func (s *Mutation) Path() string

func (*Mutation) RequestType

func (m *Mutation) RequestType() reflect.Type

RequestType is the Go type of the body; NoBody for none.

func (*Mutation) Resource

func (m *Mutation) Resource() string

Resource names the resource whose field write checks apply to the body, or "".

func (*Mutation) ResponseType

func (m *Mutation) ResponseType() reflect.Type

ResponseType is the Go type of the response; NoContent for none.

func (*Mutation) Status

func (m *Mutation) Status() int

Status is the success status.

func (*Mutation) Tables

func (s *Mutation) Tables() []string

type MutationOptions

type MutationOptions[Parameters, Request, Response any] struct {
	Check       authorization.Check
	Status      int
	ClientID    ClientIDVersion
	Resource    string
	FieldChecks map[string]authorization.Check
	Effect      Effect
	Handle      func(ctx context.Context, parameters *Parameters, body *Request) (*Response, error)
}

MutationOptions declares a write. Check and Handle are required. Status is the success status (default 200; a NoContent response defaults to 204 and may set another 2xx, 204 with a response body is refused). ClientID accepts a client-generated `id` in the body of a create (POST, D18); the body of any other method may carry an id and the handler judges it. Resource names a registered resource whose field Write checks apply to the body by JSON name (D39); FieldChecks adds a check per JSON field of Request. Effect is the optimistic effect on live lists (D36). The body is validated with `validate` tags (go-playground/validator, D44) after the structural checks; a tag the validator cannot parse, on any struct reachable from Request, is a declaration error.

type NoBody

type NoBody struct{}

NoBody is the request type of a mutation without a body; a request that sends one is refused.

type NoContent

type NoContent struct{}

NoContent is the response type of a mutation that answers without a body (204 unless Status says otherwise).

type NoParameters

type NoParameters struct{}

NoParameters is the parameters type of a declaration without parameters.

type Object

type Object struct {
	// contains filtered or unexported fields
}

Object is a read that answers one item for the typed parameters.

func NewObject

func NewObject[Item, Parameters any](name, path string, options *ObjectOptions[Item, Parameters]) *Object

NewObject declares a typed-parameters object of Item at path, whose `{name}` placeholders are the `path:"name"` fields of Parameters. The options are copied. A mistake is reported by Registry.Add (D43).

func (*Object) Check

func (s *Object) Check() authorization.Check

func (*Object) DecodeParameters

func (s *Object) DecodeParameters(source ParameterSource) (any, error)

func (*Object) Fields

func (o *Object) Fields() query.Fields

Fields are the guarded fields (name and Read check only) for RedactItem; the result is the caller's copy.

func (*Object) ItemType

func (o *Object) ItemType() reflect.Type

ItemType is the Go type of the item.

func (*Object) Kind

func (o *Object) Kind() Kind

Kind is KindObject.

func (*Object) Load

func (o *Object) Load(ctx context.Context, parameters any) (any, error)

Load runs the loader with the decoded parameters (a *Parameters); the item is a *Item. A loader that finds nothing returns NotFound.

func (*Object) Method

func (s *Object) Method() string

func (*Object) Name

func (s *Object) Name() string

func (*Object) Parameters

func (s *Object) Parameters() []Parameter

func (*Object) Partition

func (l *Object) Partition() PartitionResolver

Partition is the partition resolver of the declaration (D25), nil without one.

func (*Object) Path

func (s *Object) Path() string

func (*Object) PresencePath

func (o *Object) PresencePath() string

PresencePath is the route of the presence read (`Path/presence`, the record named by the `key` parameter within the object, D37).

func (*Object) Tables

func (s *Object) Tables() []string

type ObjectOptions

type ObjectOptions[Item, Parameters any] struct {
	Tables    []string
	Read      authorization.Check
	Partition PartitionResolver
	Guarded   []GuardedField
	Load      func(ctx context.Context, parameters *Parameters) (*Item, error)
}

ObjectOptions declares a typed-parameters object. Read and Load are required; Guarded names the fields of Item whose Read check strips them (D39). Partition names a subscription's partition (D25); nil means the tenant.

type Observer

type Observer func(ctx context.Context, outcome Outcome)

Observer receives outcomes synchronously with the context of the unit of work. It must not block and must be safe for concurrent use; a nil Observer means no reporting.

type Operation

type Operation string

Operation is a socket request's operation (D2); the JSON field is "op".

const (
	OperationQuery       Operation = "query"       // one read, answered in the response
	OperationSubscribe   Operation = "subscribe"   // a live read; the response names the subscription, pushes follow
	OperationUnsubscribe Operation = "unsubscribe" // target is the subscription
	OperationCall        Operation = "call"        // a mutation
	OperationBatch       Operation = "batch"       // several mutations in one transaction (D33)
	OperationJoin        Operation = "join"        // presence (D37)
	OperationLeave       Operation = "leave"
	OperationPing        Operation = "ping"
)

The socket operations. Uploads are REST only.

type Outcome

type Outcome struct {
	Kind         OutcomeKind
	Operation    Operation
	Target       string
	Subscription string
	Tenant       string
	Identity     *auth.Identity
	Parameters   any
	Status       int
	Err          error
	Duration     time.Duration
	Rows         int
	QueryWait    time.Duration
}

Outcome is one unit of work the engine or an adapter finished (D4, D30). Target is the declaration's name, with the read kind after a dot for a read other than the main one (D46), "batch" for a batch, "filters" for the stored filter routes, and "" when the request named no declaration (D59); Operation is set on the socket ("other" for an operation the socket does not know), empty on REST; Subscription is set for subscribe and rerun; Tenant is the resolved tenant (D42); Parameters is the decoded parameters value (a *Parameters) when they were decoded; Status is the HTTP status answered (the close code for OutcomeClose); Err is the refusal or failure, with the server-side Reason of an *auth.Error, and nil on success; Rows is the rows an export wrote; QueryWait is how long the unit waited for the engine's query slots (D52), so it is zero on REST.

type OutcomeKind

type OutcomeKind string

OutcomeKind says which unit of work an Outcome reports.

const (
	OutcomeRequest     OutcomeKind = "request"      // one REST request
	OutcomeSubscribe   OutcomeKind = "subscribe"    // a subscribe op, its first load included
	OutcomeCall        OutcomeKind = "call"         // one mutation, on REST or the socket
	OutcomeBatch       OutcomeKind = "batch"        // one batch (D33)
	OutcomeRerun       OutcomeKind = "rerun"        // a subscription re-run after a change, resync or tick
	OutcomeAfterCommit OutcomeKind = "after_commit" // a mutation's transaction committed (D41)
	OutcomeOpen        OutcomeKind = "open"         // a socket was upgraded
	OutcomeClose       OutcomeKind = "close"        // a socket ended; Status is its close code (1000, 1001, 1013, 4401, ...)
	OutcomeRevalidate  OutcomeKind = "revalidate"   // a socket's identity re-validated on a tick or a Revalidate call (D6)
	OutcomeFeed        OutcomeKind = "feed"         // the change feed failed; the engine restarts it after a backoff
	OutcomeHeartbeat   OutcomeKind = "heartbeat"    // the presence heartbeat and sweep of a tick (D37)
	OutcomeExport      OutcomeKind = "export"       // one export (D35); Rows is how many rows it wrote
	OutcomeEnd         OutcomeKind = "end"          // a subscription a subscribe opened ended: unsubscribed, by an error push or with its socket
)

The outcome kinds.

type Owner

type Owner struct {
	Key    string
	Tenant string
}

Owner scopes a stored row to who stored it: the identity key "provider:subject" and the tenant of the request (D42, "" in a single-tenant registry). Two owners are the same only when both parts are equal; a key of another owner is a fresh key (section 6).

func OwnerOf

func OwnerOf(ctx context.Context) (Owner, bool)

OwnerOf is the owner of the identity in ctx under the tenant in ctx (TenantFrom). Without an identity there is no owner (false): the engine then skips idempotency and stored filters, so anonymous callers never share a namespace (D16).

type Page

type Page[Item any] struct {
	Items []Item
	Total int
	Next  *query.Cursor
}

Page is one page a filterable resource's List loader returns (D14, D38, D55): the items in page order, the total under scope and filter (at most CountCap + 1 under a cap) and, when the page is full, the cursor after its last row (nil otherwise), unencoded. The engine answers Next encoded as ListResponse.next for a resource in cursor mode and ignores it otherwise, so a sort value without a cursor form fails only the reads that need the cursor. postgresadapter.ListPage returns one.

type Parameter

type Parameter struct {
	Name string
	In   ParameterIn
	Type reflect.Type
	Enum []string
	List bool
}

Parameter describes one parameter of a declaration for the generators: its name on the wire, where it travels, the Go type of its struct field (pointer and slice types as declared), the enum values of an `enum:"a,b"` tag, and whether the wire carries a list. A path parameter is required, a query parameter optional.

func (Parameter) Required

func (p Parameter) Required() bool

Required reports whether a request must carry the parameter.

type ParameterIn

type ParameterIn string

ParameterIn is where a parameter travels.

const (
	InPath  ParameterIn = "path"
	InQuery ParameterIn = "query"
)

The parameter locations; they are the struct tags of a parameters struct.

type ParameterSource

type ParameterSource interface {
	Path(name string) (string, bool)
	Query(name string) ([]string, bool)
}

ParameterSource hands the decoder the raw values of a request: path values by name and query values by name, where a query key may repeat. The REST adapter builds one from the route, the engine from a RequestFrame's Parameters (MapSource).

type PartitionResolver

type PartitionResolver func(ctx context.Context, parameters any) string

PartitionResolver names the partition of a subscription from the decoded parameters (a *Parameters, nil for a resource read) and the context (D25); the engine indexes subscriptions by table and partition and partitioned triggers carry the row's column. Without one the tenant of D42 is the partition.

type Presence

type Presence struct {
	Socket    string
	Owner     Owner
	Display   string
	Target    string
	Key       string
	State     json.RawMessage
	UpdatedAt time.Time
}

Presence is one socket's presence on a target record (D37): the socket, its owner and display name, the target and key that name the record, and the client's state.

type PresenceItem

type PresenceItem struct {
	ID      string          `json:"id"`
	Display string          `json:"display"`
	State   json.RawMessage `json:"state"`
}

PresenceItem is one presence as a presence read answers it: the socket that joined, its display name and the state it sent.

type PresenceStore

type PresenceStore interface {
	// Join inserts or updates the socket's presence on the record.
	Join(ctx context.Context, presence *Presence) error
	// Leave removes the socket's presence on the tenant's record; an absent one is no error.
	Leave(ctx context.Context, tenant, socket, target, key string) error
	// Heartbeat refreshes UpdatedAt of every presence of the socket.
	Heartbeat(ctx context.Context, socket string) error
	// List returns the presences on a record of the tenant; a store may bound how many it
	// returns (postgresadapter: 1000, by socket).
	List(ctx context.Context, tenant, target, key string) ([]Presence, error)
	// Sweep removes every presence older than maximumAge and returns how many.
	Sweep(ctx context.Context, maximumAge time.Duration) (int, error)
}

PresenceStore keeps the rows of D37; a change to them reaches the engine through the ChangeFeed like any other table. A record is named by tenant, target and key.

type PushFrame

type PushFrame struct {
	Subscription string          `json:"sub"`
	Sequence     int64           `json:"seq"`
	Kind         PushKind        `json:"kind"`
	Payload      json.RawMessage `json:"payload"`
}

PushFrame is one change of a subscription; Sequence counts from 1 per subscription.

type PushKind

type PushKind string

PushKind says what a push carries.

const (
	PushSnapshot   PushKind = "snapshot" // a list's full ListResponse body: the first push and after a resync
	PushListChange PushKind = "diff"     // a ListChange against the last pushed list
	PushReplace    PushKind = "replace"  // an object's full body
	PushError      PushKind = "error"    // an ErrorBody; the subscription has ended
)

The push kinds.

type ReadKind

type ReadKind string

ReadKind says which read of a declaration a request names. On the socket it is the target's suffix after a dot (D46): "parts" is the main read, "parts.item" the item read, "parts.distinct" the distinct read, "parts.history" the versions of one item, "parts.presence" the presences on one record.

const (
	ReadMain     ReadKind = ""         // a resource's or list's ListResponse, an object's item
	ReadItem     ReadKind = "item"     // a resource's item read, keyed by its ItemKey parameter
	ReadDistinct ReadKind = "distinct" // a resource's distinct read (D15), the `field` parameter names the field
	ReadHistory  ReadKind = "history"  // a versioned resource's versions of one item (D34), keyed like the item read
	ReadPresence ReadKind = "presence" // the presences on the record named by the `key` parameter (D37)
)

The read kinds.

type ReadParameters

type ReadParameters struct {
	Filter   json.RawMessage
	FilterID string
	Sort     string
	Limit    string
	Offset   string
	After    string
	Field    string
}

ReadParameters are the reserved parameters of one resource read as the client sent them. Filter is the inline document, or the stored one the engine resolved from FilterID.

func ReadParametersFrom

func ReadParametersFrom(source ParameterSource) (*ReadParameters, error)

ReadParametersFrom reads the reserved parameters from source; a repeated one is an *Error of 400 invalid_request naming it.

type ReadRequest

type ReadRequest struct {
	Target string
	Kind   ReadKind
	Source ParameterSource
}

ReadRequest is one read as a transport hands it to Engine.Read: the declaration's name, the read kind and the request's parameters, path and query.

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry is the single source of truth of an API: every declaration by name, in declaration order. Build it in main with Add; it is read-only afterwards and safe for concurrent reads.

func NewRegistry

func NewRegistry(options *RegistryOptions) (*Registry, error)

NewRegistry validates and copies options. A nil options value is the defaults.

func (*Registry) Add

func (r *Registry) Add(declarations ...Declaration) error

Add registers declarations in order. It returns the first mistake it finds: a nil or wrapped declaration, one that failed its own validation or belongs to another registry, the reserved names "batch" and "filters" (the targets of a batch's and the stored filter routes' outcomes, D33, D59), a duplicate name, a duplicate method and route pattern (placeholder names aside; a resource's item and distinct routes count), a mutation whose Resource or Effect target is not registered by the end of the call, a route of the library's own (BatchPath, FiltersPath). Nothing is added and no declaration is changed when any declaration fails, so main fails fast on a wiring mistake.

Example
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"

	"github.com/google/uuid"
	"github.com/govengers/auth"
	"github.com/govengers/auth/authorization"

	"github.com/govengers/livewire"
	"github.com/govengers/livewire/query"
)

type todo struct {
	ID        uuid.UUID `json:"id"`
	Title     string    `json:"title"`
	Done      bool      `json:"done"`
	CreatedAt time.Time `json:"createdAt"`
}

type createTodo struct {
	ID    *uuid.UUID `json:"id,omitempty"`
	Title string     `json:"title"`
}

func main() {
	registry, err := livewire.NewRegistry(&livewire.RegistryOptions{BasePath: "/api/v1"})
	if err != nil {
		fmt.Println(err)
		return
	}
	err = registry.Add(
		livewire.NewResource("todos", "/todos", &livewire.ResourceOptions[todo]{
			Tables: []string{"todos"},
			Read:   authorization.IsUser(),
			Fields: query.Fields{
				{Name: "id", Type: query.TypeUUID, SQL: "todos.id", Filter: true, Sort: true},
				{Name: "title", Type: query.TypeText, SQL: "todos.title", Filter: true, Sort: true, Search: true},
				{Name: "done", Type: query.TypeBoolean, SQL: "todos.done", Filter: true, Sort: true},
				{Name: "createdAt", Type: query.TypeTime, SQL: "todos.created_at", Filter: true, Sort: true},
			},
			DefaultSort: []query.SortTerm{{Field: "createdAt", Descending: true}},
			Item:        &livewire.ItemKey{Parameter: "id", Field: "id"},
			List: func(context.Context, *query.ListQuery) (*livewire.Page[todo], error) {
				return &livewire.Page[todo]{}, nil // the repository runs postgresadapter.ListPage
			},
			Get: func(context.Context, *query.ListQuery) (*todo, error) {
				return nil, livewire.NotFound()
			},
		}),
		livewire.NewMutation("createTodo", http.MethodPost, "/todos", &livewire.MutationOptions[livewire.NoParameters, createTodo, todo]{
			Check:    authorization.HasPermission("todos", auth.AccessWrite),
			Status:   http.StatusCreated,
			ClientID: livewire.UUIDv7,
			Resource: "todos",
			Effect:   livewire.Upserts("todos"),
			Handle: func(_ context.Context, _ *livewire.NoParameters, body *createTodo) (*todo, error) {
				return &todo{ID: uuid.New(), Title: body.Title}, nil
			},
		}),
	)
	if err != nil {
		fmt.Println(err)
		return
	}
	for _, declaration := range registry.Declarations() {
		fmt.Println(declaration.Kind(), declaration.Name(), declaration.Method(), registry.BasePath()+declaration.Path())
	}
	todos, _ := registry.Lookup("todos")
	fmt.Println(todos.(*livewire.Resource).ItemPath())
}
Output:
resource todos GET /api/v1/todos
mutation createTodo POST /api/v1/todos
/todos/{id}

func (*Registry) BasePath

func (r *Registry) BasePath() string

BasePath is the prefix of every REST route, "" for none.

func (*Registry) Declarations

func (r *Registry) Declarations() []Declaration

Declarations returns every declaration in declaration order; the result is the caller's copy.

func (*Registry) Lookup

func (r *Registry) Lookup(name string) (Declaration, bool)

Lookup returns the declaration with the given name.

func (*Registry) Observe

func (r *Registry) Observe(ctx context.Context, outcome Outcome)

Observe reports an outcome to the Observer; it does nothing without one.

func (*Registry) Tenant

func (r *Registry) Tenant(ctx context.Context, parameters any) (string, error)

Tenant resolves the tenant of a request (D42): "" and no error with a single tenant.

type RegistryOptions

type RegistryOptions struct {
	BasePath string
	Tenant   func(ctx context.Context, parameters any) (string, error)
	Observer Observer
}

RegistryOptions configures the registry. BasePath is the prefix of every REST route ("/api/v1"; empty for none) and must start with a slash and not end with one. Tenant resolves the tenant of a request from the decoded parameters (a *Parameters, nil for a resource read) and the identity in ctx (D42); nil means one tenant. Observer receives the outcomes of the engine and the adapters; nil means none.

type RequestFrame

type RequestFrame struct {
	ID             string            `json:"id"`
	Operation      Operation         `json:"op"`
	Target         string            `json:"target,omitempty"`
	Parameters     map[string]string `json:"params,omitempty"`
	Body           json.RawMessage   `json:"body,omitempty"`
	IdempotencyKey string            `json:"idempotencyKey,omitempty"`
}

RequestFrame is one client request on the socket. Target names the declaration of a read with the read kind after a dot ("parts", "parts.item", "parts.distinct", "parts.presence"; ReadKind), the mutation of a call, the declaration of a join or leave, and the subscription of an unsubscribe. Parameters are the target's parameters by name as a REST query string would carry them (a list parameter is one comma-separated value); Body is the mutation body of a call, the calls of a batch (a JSON array of BatchCall) or the state of a join; IdempotencyKey is the Idempotency-Key of a call or batch (D16).

type Resource

type Resource struct {
	// contains filtered or unexported fields
}

Resource is a filterable list of items with optional item, distinct and history reads and an export. Its routes are GET Path (list), GET ItemPath (one item), GET DistinctPath (distinct values), GET HistoryPath (one item's versions) and GET ExportPath (every row as CSV); all take the reserved parameters.

func NewResource

func NewResource[Item any](name, path string, options *ResourceOptions[Item]) *Resource

NewResource declares a filterable resource of Item under path (no placeholders). The options are copied; changing them afterwards changes nothing. A mistake in the declaration is reported by Registry.Add, never by a panic (D43).

Example (ColumnChecks)

ExampleNewResource_columnChecks guards a column (D39): an identity without the staff role reads every employee without the salary, may not filter, sort or search by it, and may not write it.

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"

	"github.com/google/uuid"
	"github.com/govengers/auth"
	"github.com/govengers/auth/authorization"

	"github.com/govengers/livewire"
	"github.com/govengers/livewire/query"
)

type employee struct {
	ID     uuid.UUID `json:"id"`
	Name   string    `json:"name"`
	Salary int       `json:"salary"`
}

type employeeParameters struct {
	ID uuid.UUID `path:"id"`
}

type updateEmployee struct {
	Salary *int    `json:"salary,omitempty"`
	Note   *string `json:"note,omitempty"`
}

func listEmployees(context.Context, *query.ListQuery) (*livewire.Page[employee], error) {
	return &livewire.Page[employee]{}, nil
}

func listPayroll(context.Context, *livewire.NoParameters) ([]employee, error) {
	return nil, nil
}

func saveEmployee(context.Context, *employeeParameters, *updateEmployee) (*employee, error) {
	return nil, livewire.NotFound()
}

func main() {
	// snippet:begin column-checks
	staff := authorization.HasRole("staff")
	employees := livewire.NewResource("employees", "/employees", &livewire.ResourceOptions[employee]{
		Tables: []string{"employees"},
		Read:   authorization.IsUser(),
		Fields: query.Fields{
			{Name: "id", Type: query.TypeUUID, SQL: "employees.id", Filter: true, Sort: true},
			{Name: "name", Type: query.TypeText, SQL: "employees.name", Filter: true, Sort: true, Search: true},
			{Name: "salary", Type: query.TypeInteger, SQL: "employees.salary", Filter: true, Sort: true, Read: staff},
		},
		DefaultSort: []query.SortTerm{{Field: "name"}},
		Cursor:      true, // keyset pages: the `after` parameter and ListResponse.next (D38)
		List:        listEmployees,
	})
	payroll := livewire.NewList("payroll", "/payroll", &livewire.ListOptions[employee, livewire.NoParameters]{
		Read:    authorization.IsUser(),
		Guarded: []livewire.GuardedField{{Name: "salary", Read: staff}}, // the same for a typed read
		Load:    listPayroll,
	})
	update := livewire.NewMutation("updateEmployee", http.MethodPatch, "/employees/{id}", &livewire.MutationOptions[employeeParameters, updateEmployee, employee]{
		Check:       authorization.IsUser(),
		Resource:    "employees", // "salary" in the body needs the field's Write check, its Read by default
		FieldChecks: map[string]authorization.Check{"note": authorization.HasRole("manager")},
		Handle:      saveEmployee,
	})
	// snippet:end column-checks
	registry, err := livewire.NewRegistry(nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	if err := registry.Add(employees, payroll, update); err != nil {
		fmt.Println(err)
		return
	}
	ada := auth.WithIdentity(context.Background(), &auth.Identity{Provider: "example", Subject: "ada", Active: true})
	visible, err := livewire.RedactItems(ada, employees.Fields(), json.RawMessage(`[{"id":"0190e3c8-0000-7000-8000-000000000001","name":"Grace","salary":100}]`))
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(string(visible))
}
Output:
[{"id":"0190e3c8-0000-7000-8000-000000000001","name":"Grace"}]

func (*Resource) Check

func (s *Resource) Check() authorization.Check

func (*Resource) Cursor

func (r *Resource) Cursor() bool

Cursor reports whether the resource pages by cursor on request (D38).

func (*Resource) DecodeParameters

func (s *Resource) DecodeParameters(source ParameterSource) (any, error)

func (*Resource) DefaultSort

func (r *Resource) DefaultSort() []query.SortTerm

DefaultSort is the sort of a read that sends none; the result is the caller's copy.

func (*Resource) DistinctPath

func (r *Resource) DistinctPath() string

DistinctPath is the route of the distinct read (`Path/distinct`), or "" without a Distinct loader.

func (*Resource) DistinctQuery

func (r *Resource) DistinctQuery(ctx context.Context, parameters *ReadParameters) (*query.ListQuery, query.Field, error)

DistinctQuery is ListQuery for a distinct read (D15): it also resolves the `field` parameter, which must name a visible filterable scalar field, and caps the page at DistinctMaximum.

func (*Resource) ExportColumns

func (r *Resource) ExportColumns() []string

ExportColumns are the fields an export writes, every field when empty (D35); the result is the caller's copy.

func (*Resource) ExportPath

func (r *Resource) ExportPath() string

ExportPath is the route of the export (`Path/export`), or "" without an Export loader.

func (*Resource) Fields

func (r *Resource) Fields() query.Fields

Fields are the declared fields with Nullable derived from the item type; the result is the caller's copy.

func (*Resource) HistoryPath

func (r *Resource) HistoryPath() string

HistoryPath is the route of the history read (`ItemPath/history`), or "" without a History loader.

func (*Resource) Item

func (r *Resource) Item() *ItemKey

Item is the item key, or nil when the resource has no item read.

func (*Resource) ItemPath

func (r *Resource) ItemPath() string

ItemPath is the route of the item read (`Path/{Item.Parameter}`), or "" without one.

func (*Resource) ItemQuery

func (r *Resource) ItemQuery(ctx context.Context, source ParameterSource) (*query.ListQuery, error)

ItemQuery is the loader's query of an item read: the item key's value from source as the one condition, under scope. A key of the wrong form is 400; a guarded key field is 403.

func (*Resource) ItemType

func (r *Resource) ItemType() reflect.Type

ItemType is the Go type of one item.

func (*Resource) Kind

func (r *Resource) Kind() Kind

Kind is KindResource.

func (*Resource) ListQuery

func (r *Resource) ListQuery(ctx context.Context, parameters *ReadParameters) (*query.ListQuery, error)

ListQuery turns the reserved parameters of a list read into the loader's query under the identity in ctx. The whole request is parsed first against every field, so an unknown field, a bad page or a bad cursor is 400 (invalid_*) whoever asks; then the fields the filter and the sort name are checked against the column checks, so a guarded one is 403 (forbidden_field, D39); search runs over the visible searchable fields only. An empty sort is the default sort; the page is clamped to MaximumLimit; `after` is refused unless the resource pages by cursor. Scope runs last.

func (*Resource) LoadDistinct

func (r *Resource) LoadDistinct(ctx context.Context, field string, listQuery *query.ListQuery) ([]DistinctValue, error)

LoadDistinct runs the Distinct loader for the field DistinctQuery resolved; the values are never nil.

func (*Resource) LoadItem

func (r *Resource) LoadItem(ctx context.Context, listQuery *query.ListQuery) (any, error)

LoadItem runs the Get loader; the item is a *Item. A loader that finds no row returns NotFound.

func (*Resource) LoadList

func (r *Resource) LoadList(ctx context.Context, listQuery *query.ListQuery) (any, error)

LoadList runs the List loader; the page is the loader's *Page[Item], a copy whose Items are never nil. A loader that returns no page and no error fails.

func (*Resource) Method

func (s *Resource) Method() string

func (*Resource) Name

func (s *Resource) Name() string

func (*Resource) Parameters

func (s *Resource) Parameters() []Parameter

func (*Resource) Partition

func (l *Resource) Partition() PartitionResolver

Partition is the partition resolver of the declaration (D25), nil without one.

func (*Resource) Path

func (s *Resource) Path() string

func (*Resource) PresencePath

func (r *Resource) PresencePath() string

PresencePath is the route of the presence read (`Path/presence`, the record named by the `key` parameter, D37), or "" without an item read.

func (*Resource) Tables

func (s *Resource) Tables() []string

func (*Resource) Versioned

func (r *Resource) Versioned() bool

Versioned reports whether the item carries `version` and a history (D34).

type ResourceOptions

type ResourceOptions[Item any] struct {
	Tables          []string
	Read            authorization.Check
	Partition       PartitionResolver
	Fields          query.Fields
	DefaultSort     []query.SortTerm
	Scope           func(ctx context.Context) (query.Predicate, error)
	MaximumLimit    int
	CountCap        int
	DistinctMaximum int
	Item            *ItemKey
	Versioned       bool
	ExportColumns   []string
	Cursor          bool
	List            func(ctx context.Context, listQuery *query.ListQuery) (*Page[Item], error)
	Get             func(ctx context.Context, listQuery *query.ListQuery) (*Item, error)
	Distinct        func(ctx context.Context, field string, listQuery *query.ListQuery) ([]DistinctValue, error)
	History         func(ctx context.Context, listQuery *query.ListQuery) (*Page[Version[Item]], error)
	Export          func(ctx context.Context, listQuery *query.ListQuery, columns query.Fields, writer io.Writer) (int, error)
}

ResourceOptions declares a filterable resource (D11). Read and List are required; Fields must validate (query.Fields.Validate) and a field left Nullable false is derived from the item type: a pointer field of the item is nullable. Get is required with Item and refused without it. Scope narrows every read and re-run before filter and count; nil is no scope. MaximumLimit clamps the page (default 100), CountCap caps the total (0 counts every row), DistinctMaximum caps a distinct read (default 1000). Cursor allows keyset pagination with the `after` parameter and answers the Next of List's Page as ListResponse.next (D38, D55). Versioned marks the item as carrying `version` (D34); History, which needs Versioned and the Item key on the uuid field "id" (its SQL the table's id column), reads one item's versions for the history route: its query is the reserved filter, sort and page over HistoryFields, scoped to the history table's `id = key`, and the subscription of it re-runs on the resource's Tables, which every version is written with. Export streams every row under scope, filter and sort to the writer for the export route (D35) and returns how many it wrote: the columns are ExportColumns, or every field, without those whose Read check refuses the caller, and a repository passes them to postgresadapter.WriteCSV, which writes a text a spreadsheet would run as a formula with a single quote in front (D69). Partition names a subscription's partition (D25); nil means the tenant.

type ResponseFrame

type ResponseFrame struct {
	ID           string          `json:"id"`
	OK           bool            `json:"ok"`
	Status       int             `json:"status"`
	Payload      json.RawMessage `json:"payload,omitempty"`
	Error        *ErrorBody      `json:"error,omitempty"`
	Subscription string          `json:"subscription,omitempty"`
	FilterID     string          `json:"filterId,omitempty"`
}

ResponseFrame answers one request by its ID. Status is the HTTP status the same request gets on REST; Payload is byte-for-byte the REST body on success, Error the body of a refusal. Subscription names the subscription a subscribe opened, FilterID the stored filter an inline filter was registered as (D13).

type StoredFilter

type StoredFilter struct {
	ID         string
	Owner      Owner
	Resource   string
	Context    string
	Name       string
	Document   json.RawMessage
	Hash       string
	CreatedAt  time.Time
	LastUsedAt time.Time
}

StoredFilter is a filter document a client stored for a resource (D13). Context is the app's dimension of the dedup key (a view or page name, "" for none); Name is empty for an unnamed filter, which is swept after 30 days without use; Hash is query.Hash of Document. Dedup is per owner, resource, context and hash.

type Transaction

type Transaction interface {
	Commit(ctx context.Context) error
	Rollback(ctx context.Context) error
	Handle() any
}

Transaction is an engine transaction (D31, D42, D45) as the core sees it. Only the engine holds it: exactly one of Commit and Rollback is called, once. Handle is the query-only value the engine puts into the unit of work's context (WithTransaction): a value that can run statements in the transaction but has no Commit or Rollback (postgresadapter hands out sqlc's DBTX method set, never the pgx.Tx), so a handler or loader cannot end the engine's transaction.

type TransactionDetails

type TransactionDetails struct {
	Tenant     string
	Identity   *auth.Identity
	Mutation   string
	Parameters any
	RequestID  string
	ReadOnly   bool
}

TransactionDetails is what a transaction records for the triggers and policies (D31, D32, D42): the tenant (SET LOCAL livewire.tenant, "" with a single tenant), the effective identity (its Real() is the real actor), the mutation's name and decoded parameters and the request id (SET LOCAL livewire.actor). ReadOnly marks the REPEATABLE READ READ ONLY transaction of a read (D45: every engine unit of work has one): it carries the tenant and no actor; Mutation is empty then and for a batch's outer transaction.

type Transactor

type Transactor interface {
	Begin(ctx context.Context, transactionDetails TransactionDetails) (Transaction, error)
}

Transactor begins an engine transaction (D31, D42). The implementation sets the tenant and, for a write, the actor of transactionDetails (SET LOCAL) before it returns, so triggers and Row Level Security policies see them.

type Upload

type Upload struct {
	// contains filtered or unexported fields
}

Upload is a binary write: REST only, POST, multipart or raw body, declared content types and size limit, typed parameters, JSON response.

func NewUpload

func NewUpload[Parameters, Response any](name, path string, options *UploadOptions[Parameters, Response]) *Upload

NewUpload declares a binary write at path, whose `{name}` placeholders are the `path:"name"` fields of Parameters. The options are copied. A mistake is reported by Registry.Add (D43).

func (*Upload) Accepts

func (u *Upload) Accepts(contentType string) string

Accepts returns the declared media type a Content-Type header value matches, parameters ignored, or "" when none does.

func (*Upload) Check

func (s *Upload) Check() authorization.Check

func (*Upload) ContentTypes

func (u *Upload) ContentTypes() []string

ContentTypes are the accepted media types; the result is the caller's copy.

func (*Upload) DecodeParameters

func (s *Upload) DecodeParameters(source ParameterSource) (any, error)

func (*Upload) Handle

func (u *Upload) Handle(ctx context.Context, parameters any, file *File) (any, error)

Handle runs the handler with the decoded parameters (a *Parameters) and the file; the response is a *Response.

func (*Upload) Kind

func (u *Upload) Kind() Kind

Kind is KindUpload.

func (*Upload) MaximumBytes

func (u *Upload) MaximumBytes() int64

MaximumBytes is the size limit of one upload.

func (*Upload) Method

func (s *Upload) Method() string

func (*Upload) Name

func (s *Upload) Name() string

func (*Upload) Parameters

func (s *Upload) Parameters() []Parameter

func (*Upload) ParametersType

func (u *Upload) ParametersType() reflect.Type

ParametersType is the Go type of the parameters struct.

func (*Upload) Path

func (s *Upload) Path() string

func (*Upload) ResponseType

func (u *Upload) ResponseType() reflect.Type

ResponseType is the Go type of the response.

func (*Upload) Status

func (u *Upload) Status() int

Status is the success status.

func (*Upload) Tables

func (s *Upload) Tables() []string

type UploadOptions

type UploadOptions[Parameters, Response any] struct {
	Check        authorization.Check
	Status       int
	ContentTypes []string
	MaximumBytes int64
	Handle       func(ctx context.Context, parameters *Parameters, file *File) (*Response, error)
}

UploadOptions declares a binary write (D17). Check, ContentTypes, MaximumBytes and Handle are required; Status is the success status (default 200).

type UploadRequest

type UploadRequest struct {
	Target         string
	Source         ParameterSource
	File           *File
	IdempotencyKey string
	RequestID      string
}

UploadRequest is one upload as the REST adapter hands it to Engine.Upload: the upload's name, the parameters, the file it read (content type, size and body) and, as for a call, the Idempotency-Key and the request id.

type Version

type Version[Item any] struct {
	ID        int             `json:"id"`
	Operation ChangeOperation `json:"operation"`
	ValidFrom time.Time       `json:"validFrom"`
	ValidTo   *time.Time      `json:"validTo"`
	Actor     string          `json:"actor"`
	RealActor string          `json:"realActor"`
	Mutation  string          `json:"mutation"`
	RequestID string          `json:"requestId"`
	Data      *Item           `json:"data"`
}

Version is one version of an item of a versioned resource as its history read answers it (D34), one row of postgresadapter.HistorySQL's `<table>_history`: ID is the version number (1, 2, ... per item), the key a live history is diffed by; Operation is what the write did; ValidFrom and ValidTo bound the time the version was current (ValidTo nil for the current one; a delete is a closed version of the last row); Actor, RealActor, Mutation and RequestID are what the engine's transaction recorded (D31; empty outside one); Data is the item as the version left it, in the item's JSON shape: the loader maps the stored row to it. Data passes the resource's column checks like every item (D39).

Directories

Path Synopsis
Package client is the runtime of the Go clients the generator writes (D19, D67): the REST requests with the API key (D20), the error body, the Idempotency-Key (D16), the export trailer (D60), the stored filters with the recovery of an evicted one (D54), the filter document builders (D12), and the one socket that carries every subscription of a Client, with reconnect and resubscribe.
Package client is the runtime of the Go clients the generator writes (D19, D67): the REST requests with the API key (D20), the error body, the Idempotency-Key (D16), the export trailer (D60), the stored filters with the recovery of an evicted one (D54), the filter document builders (D12), and the one socket that carries every subscription of a Client, with reconnect and resubscribe.
Package generate turns a livewire registry into its artifacts (D19): the TypeScript client (types.ts, filters.ts, api.ts and the copied runtime.ts and vue.ts), the OpenAPI 3.0.3 document, the Go client package on livewire/client (D67) and the command-line client on github.com/urfave/cli/v3 (D68).
Package generate turns a livewire registry into its artifacts (D19): the TypeScript client (types.ts, filters.ts, api.ts and the copied runtime.ts and vue.ts), the OpenAPI 3.0.3 document, the Go client package on livewire/client (D67) and the command-line client on github.com/urfave/cli/v3 (D68).
internal/fixture
Package fixture holds the DTOs of the generator's test registry: every mapping case of encoding/json the generator follows.
Package fixture holds the DTOs of the generator's test registry: every mapping case of encoding/json the generator follows.
internal/fixture/billing
Package billing holds DTOs whose Go names repeat the fixture package's: the generator qualifies both with their package's name.
Package billing holds DTOs whose Go names repeat the fixture package's: the generator qualifies both with their package's name.
internal/fixture/fixtureclient
Package fixtureclient is the Go client of the API at /api/v1 (D19, D67), generated from its livewire registry: build a Client with New and call one method per read, mutation, upload and export.
Package fixtureclient is the Go client of the API at /api/v1 (D19, D67), generated from its livewire registry: build a Client with New and call one method per read, mutation, upload and export.
internal/fixture/fixturecommand
Package fixturecommand is the command-line client fixture of the API at /api/v1 (D19, D20), on github.com/urfave/cli/v3, generated from its livewire registry: one command per declaration and read kind, the parameters as flags, a body as --body or on stdin, the answers as JSON on stdout and the diagnostics on stderr.
Package fixturecommand is the command-line client fixture of the API at /api/v1 (D19, D20), on github.com/urfave/cli/v3, generated from its livewire registry: one command per declaration and read kind, the parameters as flags, a body as --body or on stdin, the answers as JSON on stdout and the diagnostics on stderr.
Package ginadapter serves a livewire engine on gin (D3): RegisterRoutes mounts the REST routes of every declaration and of the library itself, SocketHandler is the socket endpoint as a gin handler, and every REST outcome is recorded on the request's wide event (D4), which the wide-event-logger's gin middleware put into the request.
Package ginadapter serves a livewire engine on gin (D3): RegisterRoutes mounts the REST routes of every declaration and of the library itself, SocketHandler is the socket endpoint as a gin handler, and every REST outcome is recorded on the request's wide event (D4), which the wide-event-logger's gin middleware put into the request.
internal
jsonfield
Package jsonfield reads the fields of a struct type the way encoding/json marshals them, for the declarations of the root package and the generators.
Package jsonfield reads the fields of a struct type the way encoding/json marshals them, for the declarations of the root package and the generators.
listkey
Package listkey is the one rule a list keys its items by (D67): the core's ListState, which diffs a list, and the Go client, which applies the diffs, both key with ItemID, as the TypeScript runtime's itemID does in the browser.
Package listkey is the one rule a list keys its items by (D67): the core's ListState, which diffs a list, and the Go client, which applies the diffs, both key with ItemID, as the TypeScript runtime's itemID does in the browser.
Package livewiretest holds the contract gates an application's test suite runs (D23, D70): GeneratedIsCurrent fails when a committed generated file differs from what the registry renders now; Parity fails when a read's REST body differs from its socket payload; Stale fails when a committed write leaves a live read's subscription behind its REST read; and TenantIsolation fails when one tenant reads a row of another (D42).
Package livewiretest holds the contract gates an application's test suite runs (D23, D70): GeneratedIsCurrent fails when a committed generated file differs from what the registry renders now; Parity fails when a read's REST body differs from its socket payload; Stale fails when a committed write leaves a live read's subscription behind its REST read; and TenantIsolation fails when one tenant reads a row of another (D42).
Package postgresadapter is the PostgreSQL side of livewire on pgx/v5: the LISTEN change feed, the trigger and table SQL for the application's migrations, the mutation transactor, and the audit, filter, idempotency and presence stores.
Package postgresadapter is the PostgreSQL side of livewire on pgx/v5: the LISTEN change feed, the trigger and table SQL for the application's migrations, the mutation transactor, and the audit, filter, idempotency and presence stores.
Package prometheusadapter exports the live engine's outcomes as Prometheus collectors (D3): New registers them on a Registerer the application passes in, never on the default registry, and Metrics.Observe is the livewire.Observer that feeds them (D49).
Package prometheusadapter exports the live engine's outcomes as Prometheus collectors (D3): New registers them on a Registerer the application passes in, never on the default registry, and Metrics.Observe is the livewire.Observer that feeds them (D49).
Package query is the filter model of filterable resources: declared fields, the filter document (and, or, not, per-type operators, search, relative times), its validation, canonical form and hash, and the compilation to SQL text with arguments.
Package query is the filter model of filterable resources: declared fields, the filter document (and, or, not, per-type operators, search, relative times), its validation, canonical form and hash, and the compilation to SQL text with arguments.

Jump to

Keyboard shortcuts

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