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 ¶
- Constants
- func HistoryFields() query.Fields
- func IdempotencyKeyFrom(ctx context.Context) string
- func OwnerKey(identity *auth.Identity) string
- func RedactItem(ctx context.Context, fields query.Fields, raw json.RawMessage) (json.RawMessage, error)
- func RedactItems(ctx context.Context, fields query.Fields, raw json.RawMessage) (json.RawMessage, error)
- func TenantFrom(ctx context.Context) string
- func TransactionFrom(ctx context.Context) any
- func WithIdempotencyKey(ctx context.Context, key string) context.Context
- func WithTenant(ctx context.Context, tenant string) context.Context
- func WithTransaction(ctx context.Context, handle any) context.Context
- type AuditEntry
- type AuditStore
- type BatchCall
- type BatchRequest
- type BatchResult
- type CallDescriber
- type CallRequest
- type Change
- type ChangeFeed
- type ChangeOperation
- type ClientIDVersion
- type Code
- type Declaration
- type DistinctValue
- type Effect
- type EffectKind
- type Engine
- func (e *Engine) Batch(ctx context.Context, request BatchRequest) (json.RawMessage, error)
- func (e *Engine) Call(ctx context.Context, request CallRequest) (status int, payload json.RawMessage, err error)
- func (e *Engine) Close(ctx context.Context) error
- func (e *Engine) DeleteFilter(ctx context.Context, id string) error
- func (e *Engine) Export(ctx context.Context, request ExportRequest) (rows int, err error)
- func (e *Engine) Handler() http.Handler
- func (e *Engine) ListFilters(ctx context.Context, resource string) (json.RawMessage, error)
- func (e *Engine) Read(ctx context.Context, request ReadRequest) (json.RawMessage, error)
- func (e *Engine) Registry() *Registry
- func (e *Engine) RevalidateAll()
- func (e *Engine) RevalidateOwner(key string)
- func (e *Engine) Run(ctx context.Context) error
- func (e *Engine) SaveFilter(ctx context.Context, body []byte) (json.RawMessage, error)
- func (e *Engine) ServesPresence() bool
- func (e *Engine) Upload(ctx context.Context, request UploadRequest) (status int, payload json.RawMessage, err error)
- type EngineOptions
- type Error
- type ErrorBody
- type ExportRequest
- type File
- type FilterItem
- type FilterRequest
- type FilterStore
- type GuardedField
- type IdempotencyRecord
- type IdempotencyStore
- type IdentitySource
- type ItemKey
- type Kind
- type List
- func (s *List) Check() authorization.Check
- func (s *List) DecodeParameters(source ParameterSource) (any, error)
- func (l *List) Fields() query.Fields
- func (l *List) ItemType() reflect.Type
- func (l *List) Kind() Kind
- func (l *List) Load(ctx context.Context, parameters any) (any, error)
- func (s *List) Method() string
- func (s *List) Name() string
- func (s *List) Parameters() []Parameter
- func (l *List) Partition() PartitionResolver
- func (s *List) Path() string
- func (s *List) Tables() []string
- type ListChange
- type ListOptions
- type ListResponse
- type ListState
- type MapSource
- type Mutation
- func (s *Mutation) Check() authorization.Check
- func (m *Mutation) ClientID() ClientIDVersion
- func (m *Mutation) DecodeBody(ctx context.Context, raw []byte) (any, error)
- func (s *Mutation) DecodeParameters(source ParameterSource) (any, error)
- func (m *Mutation) Effect() Effect
- func (m *Mutation) Handle(ctx context.Context, parameters, body any) (any, error)
- func (m *Mutation) HasBody() bool
- func (m *Mutation) HasContent() bool
- func (m *Mutation) Kind() Kind
- func (s *Mutation) Method() string
- func (s *Mutation) Name() string
- func (s *Mutation) Parameters() []Parameter
- func (m *Mutation) ParametersType() reflect.Type
- func (s *Mutation) Path() string
- func (m *Mutation) RequestType() reflect.Type
- func (m *Mutation) Resource() string
- func (m *Mutation) ResponseType() reflect.Type
- func (m *Mutation) Status() int
- func (s *Mutation) Tables() []string
- type MutationOptions
- type NoBody
- type NoContent
- type NoParameters
- type Object
- func (s *Object) Check() authorization.Check
- func (s *Object) DecodeParameters(source ParameterSource) (any, error)
- func (o *Object) Fields() query.Fields
- func (o *Object) ItemType() reflect.Type
- func (o *Object) Kind() Kind
- func (o *Object) Load(ctx context.Context, parameters any) (any, error)
- func (s *Object) Method() string
- func (s *Object) Name() string
- func (s *Object) Parameters() []Parameter
- func (l *Object) Partition() PartitionResolver
- func (s *Object) Path() string
- func (o *Object) PresencePath() string
- func (s *Object) Tables() []string
- type ObjectOptions
- type Observer
- type Operation
- type Outcome
- type OutcomeKind
- type Owner
- type Page
- type Parameter
- type ParameterIn
- type ParameterSource
- type PartitionResolver
- type Presence
- type PresenceItem
- type PresenceStore
- type PushFrame
- type PushKind
- type ReadKind
- type ReadParameters
- type ReadRequest
- type Registry
- func (r *Registry) Add(declarations ...Declaration) error
- func (r *Registry) BasePath() string
- func (r *Registry) Declarations() []Declaration
- func (r *Registry) Lookup(name string) (Declaration, bool)
- func (r *Registry) Observe(ctx context.Context, outcome Outcome)
- func (r *Registry) Tenant(ctx context.Context, parameters any) (string, error)
- type RegistryOptions
- type RequestFrame
- type Resource
- func (s *Resource) Check() authorization.Check
- func (r *Resource) Cursor() bool
- func (s *Resource) DecodeParameters(source ParameterSource) (any, error)
- func (r *Resource) DefaultSort() []query.SortTerm
- func (r *Resource) DistinctPath() string
- func (r *Resource) DistinctQuery(ctx context.Context, parameters *ReadParameters) (*query.ListQuery, query.Field, error)
- func (r *Resource) ExportColumns() []string
- func (r *Resource) ExportPath() string
- func (r *Resource) Fields() query.Fields
- func (r *Resource) HistoryPath() string
- func (r *Resource) Item() *ItemKey
- func (r *Resource) ItemPath() string
- func (r *Resource) ItemQuery(ctx context.Context, source ParameterSource) (*query.ListQuery, error)
- func (r *Resource) ItemType() reflect.Type
- func (r *Resource) Kind() Kind
- func (r *Resource) ListQuery(ctx context.Context, parameters *ReadParameters) (*query.ListQuery, error)
- func (r *Resource) LoadDistinct(ctx context.Context, field string, listQuery *query.ListQuery) ([]DistinctValue, error)
- func (r *Resource) LoadItem(ctx context.Context, listQuery *query.ListQuery) (any, error)
- func (r *Resource) LoadList(ctx context.Context, listQuery *query.ListQuery) (any, error)
- func (s *Resource) Method() string
- func (s *Resource) Name() string
- func (s *Resource) Parameters() []Parameter
- func (l *Resource) Partition() PartitionResolver
- func (s *Resource) Path() string
- func (r *Resource) PresencePath() string
- func (s *Resource) Tables() []string
- func (r *Resource) Versioned() bool
- type ResourceOptions
- type ResponseFrame
- type StoredFilter
- type Transaction
- type TransactionDetails
- type Transactor
- type Upload
- func (u *Upload) Accepts(contentType string) string
- func (s *Upload) Check() authorization.Check
- func (u *Upload) ContentTypes() []string
- func (s *Upload) DecodeParameters(source ParameterSource) (any, error)
- func (u *Upload) Handle(ctx context.Context, parameters any, file *File) (any, error)
- func (u *Upload) Kind() Kind
- func (u *Upload) MaximumBytes() int64
- func (s *Upload) Method() string
- func (s *Upload) Name() string
- func (s *Upload) Parameters() []Parameter
- func (u *Upload) ParametersType() reflect.Type
- func (s *Upload) Path() string
- func (u *Upload) ResponseType() reflect.Type
- func (u *Upload) Status() int
- func (s *Upload) Tables() []string
- type UploadOptions
- type UploadRequest
- type Version
Examples ¶
Constants ¶
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).
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.
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.
const ( DefaultMaximumLimit = 100 DefaultDistinctMaximum = 1000 )
Default limits of a filterable resource.
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.
const ExportCSV = "csv"
ExportCSV is the format of a CSV export.
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.
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.
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 ¶
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 ¶
IdempotencyKeyFrom returns the Idempotency-Key of the mutation in ctx, or "" outside one.
func OwnerKey ¶
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 ¶
TenantFrom returns the tenant of the request in ctx, "" in a single-tenant registry or outside a request.
func TransactionFrom ¶
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 ¶
WithIdempotencyKey returns a copy of ctx carrying the Idempotency-Key of the call.
func WithTenant ¶
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 ¶
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 ¶
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.
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) }()
}
Output:
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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) 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 ¶
RevalidateOwner is RevalidateAll for the sockets whose effective or real identity has the owner key (OwnerKey).
func (*Engine) Run ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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 ¶
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 ¶
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 ¶
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 ¶
Fields are the guarded fields (name and Read check only) for RedactItems; the result is the caller's copy.
func (*List) Load ¶
Load runs the loader with the decoded parameters (a *Parameters); the items are a []Item, never nil.
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.
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 ¶
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.
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 ¶
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) Handle ¶
Handle runs the handler with the decoded parameters (a *Parameters) and body (a *Request); the response is a *Response.
func (*Mutation) HasContent ¶
HasContent reports whether the mutation answers with a body.
func (*Mutation) Parameters ¶
func (s *Mutation) Parameters() []Parameter
func (*Mutation) ParametersType ¶
ParametersType is the Go type of the parameters struct.
func (*Mutation) RequestType ¶
RequestType is the Go type of the body; NoBody for none.
func (*Mutation) Resource ¶
Resource names the resource whose field write checks apply to the body, or "".
func (*Mutation) ResponseType ¶
ResponseType is the Go type of the response; NoContent for none.
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 ¶
Fields are the guarded fields (name and Read check only) for RedactItem; the result is the caller's copy.
func (*Object) Load ¶
Load runs the loader with the decoded parameters (a *Parameters); the item is a *Item. A loader that finds nothing returns NotFound.
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) PresencePath ¶
PresencePath is the route of the presence read (`Path/presence`, the record named by the `key` parameter within the object, D37).
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 ¶
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 ¶
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).
type Page ¶
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 ¶
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.
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 ¶
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) 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.
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) DecodeParameters ¶
func (s *Resource) DecodeParameters(source ParameterSource) (any, error)
func (*Resource) DefaultSort ¶
DefaultSort is the sort of a read that sends none; the result is the caller's copy.
func (*Resource) DistinctPath ¶
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 ¶
ExportColumns are the fields an export writes, every field when empty (D35); the result is the caller's copy.
func (*Resource) ExportPath ¶
ExportPath is the route of the export (`Path/export`), or "" without an Export loader.
func (*Resource) Fields ¶
Fields are the declared fields with Nullable derived from the item type; the result is the caller's copy.
func (*Resource) HistoryPath ¶
HistoryPath is the route of the history read (`ItemPath/history`), or "" without a History loader.
func (*Resource) ItemPath ¶
ItemPath is the route of the item read (`Path/{Item.Parameter}`), or "" without one.
func (*Resource) ItemQuery ¶
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) 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 ¶
LoadItem runs the Get loader; the item is a *Item. A loader that finds no row returns NotFound.
func (*Resource) LoadList ¶
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) 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) PresencePath ¶
PresencePath is the route of the presence read (`Path/presence`, the record named by the `key` parameter, D37), or "" without an item read.
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 ¶
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 ¶
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 ¶
Handle runs the handler with the decoded parameters (a *Parameters) and the file; the response is a *Response.
func (*Upload) MaximumBytes ¶
MaximumBytes is the size limit of one upload.
func (*Upload) Parameters ¶
func (s *Upload) Parameters() []Parameter
func (*Upload) ParametersType ¶
ParametersType is the Go type of the parameters struct.
func (*Upload) ResponseType ¶
ResponseType is the Go type of the response.
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).
Source Files
¶
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. |