Documentation
¶
Index ¶
- Constants
- Variables
- func CountAuthor(ctx context.Context, db DB, where []Condition) (int64, error)
- func CountOrg(ctx context.Context, db DB, where []Condition) (int64, error)
- func CountPost(ctx context.Context, db DB, where []Condition) (int64, error)
- func DeleteAuthor(ctx context.Context, db DB, id any, where []Condition) error
- func DeleteOrg(ctx context.Context, db DB, id any, where []Condition) error
- func DeletePost(ctx context.Context, db DB, id any, where []Condition) error
- func ParseBool(s string) (any, error)
- func ParseFloat(s string) (any, error)
- func ParseInt(s string) (any, error)
- func ParseText(s string) (any, error)
- func ParseTime(s string) (any, error)
- func Register(mux *http.ServeMux, db DB, opts Options) error
- func WriteJSON(w http.ResponseWriter, status int, body any)
- func WriteProblem(w http.ResponseWriter, err error)
- type Author
- func GetAuthor(ctx context.Context, db DB, id any, where []Condition) (Author, error)
- func InsertAuthor(ctx context.Context, db DB, values map[string]any) (Author, error)
- func ListAuthor(ctx context.Context, db DB, q Query) ([]Author, error)
- func UpdateAuthor(ctx context.Context, db DB, id any, changes map[string]any, where []Condition) (Author, error)
- type AuthorsHooks
- type Column
- type Condition
- type DB
- type Limits
- type ListRequest
- type Options
- type Order
- type Org
- func GetOrg(ctx context.Context, db DB, id any, where []Condition) (Org, error)
- func InsertOrg(ctx context.Context, db DB, values map[string]any) (Org, error)
- func ListOrg(ctx context.Context, db DB, q Query) ([]Org, error)
- func UpdateOrg(ctx context.Context, db DB, id any, changes map[string]any, where []Condition) (Org, error)
- type OrgsHooks
- type Page
- type Post
- func GetPost(ctx context.Context, db DB, id any, where []Condition) (Post, error)
- func InsertPost(ctx context.Context, db DB, values map[string]any) (Post, error)
- func ListPost(ctx context.Context, db DB, q Query) ([]Post, error)
- func UpdatePost(ctx context.Context, db DB, id any, changes map[string]any, where []Condition) (Post, error)
- type PostsHooks
- type Problem
- type ProblemDetail
- type Query
Constants ¶
const ( OpEq = "=" OpNe = "<>" OpLt = "<" OpLte = "<=" OpGt = ">" OpGte = ">=" OpIn = "IN" OpNotIn = "NOT IN" OpIsNull = "IS NULL" OpNotNull = "IS NOT NULL" OpLike = "LIKE" OpILike = "ILIKE" OpBetween = "BETWEEN" )
The operators, as the SQL each one becomes.
Variables ¶
var ( // ErrNotFound is the id that matched nothing the request was allowed to // see. Handlers turn it into a 404 without saying which of the two it was. ErrNotFound = errors.New("not found") // ErrNoChanges is a PATCH that named no column. ErrNoChanges = errors.New("no columns to change") )
var Schema string
Schema is schema.sql, embedded so that a test or a bootstrap can apply the whole schema without knowing where the file ended up.
Functions ¶
func CountAuthor ¶
CountAuthor is ?count=exact: the size of the matching set, which costs a second query over the same predicate.
func CountOrg ¶
CountOrg is ?count=exact: the size of the matching set, which costs a second query over the same predicate.
func CountPost ¶
CountPost is ?count=exact: the size of the matching set, which costs a second query over the same predicate.
func DeleteAuthor ¶
DeleteAuthor removes one row, and reports ErrNotFound rather than success when the id matched nothing the conditions admit.
func DeleteOrg ¶
DeleteOrg removes one row, and reports ErrNotFound rather than success when the id matched nothing the conditions admit.
func DeletePost ¶
DeletePost removes one row, and reports ErrNotFound rather than success when the id matched nothing the conditions admit.
func ParseFloat ¶
func ParseTime ¶
ParseTime accepts RFC 3339 and a bare date, which are the two spellings a client sends for a timestamp and a date column.
func Register ¶
Register mounts every resource the schema exposed.
It returns an error rather than panicking, and it returns one for a missing obligation before it registers anything: a resource that declared a tenant column and has nothing to confine it with would serve every tenant's rows with a 200 next to them, and that is the failure this check exists for.
func WriteJSON ¶
func WriteJSON(w http.ResponseWriter, status int, body any)
WriteJSON writes a success response.
func WriteProblem ¶
func WriteProblem(w http.ResponseWriter, err error)
WriteProblem writes an error response. Anything that is not already a Problem, and is not an integrity violation the database named, is a 500 whose detail is not the caller's business.
Types ¶
type Author ¶
type Author struct {
ID string `db:"id" json:"id"`
OrgID string `db:"org_id" json:"org_id"`
Email string `db:"email" json:"email"`
Name string `db:"name" json:"name"`
PasswordHash string `db:"password_hash" json:"-"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
Author is a row of authors.
func GetAuthor ¶
GetAuthor reads one row by primary key. The extra conditions are whatever confines this table — a tenant, a soft delete — and they are part of the lookup rather than a check afterwards, so a row outside them is a 404 and not a 403 that confirms it exists.
func InsertAuthor ¶
InsertAuthor writes one row and reads it back, so database defaults and computed columns arrive without a second query.
The column order is the schema's, not the map's: a generated statement whose text depends on map iteration is a statement that cannot be diffed.
func ListAuthor ¶
ListAuthor reads a page. It returns one row more than asked for when there is one, which is how the handler answers has_more without a second count.
func UpdateAuthor ¶
func UpdateAuthor(ctx context.Context, db DB, id any, changes map[string]any, where []Condition) (Author, error)
UpdateAuthor writes the named columns of one row and reads the row back. An empty change set is the caller's mistake rather than a statement with no SET clause, which Postgres will not parse.
type AuthorsHooks ¶
type AuthorsHooks struct {
// Confine narrows every statement this resource issues. Nil means
// unconfined, which is only allowed when the schema declared nothing.
Confine func(*http.Request) ([]Condition, error)
// Assign supplies column values a create must set that no request body
// carries. It runs before the insert and its values win.
Assign func(*http.Request) (map[string]any, error)
}
AuthorsHooks are the seams for /authors.
type Column ¶
type Column struct {
Name string
Filterable bool
Sortable bool
Searchable bool
// Parse turns a query-string value into something pgx can bind.
Parse func(string) (any, error)
}
Column is what a request may name, and for what. The capabilities are the ones the schema declared: a column that never opted into filtering is not filterable here either, and the rejection says which columns are.
type Condition ¶
Condition is one predicate. Or holds a disjunction — ?search fans out over the searchable columns and is the only thing that produces one.
type DB ¶
type DB interface {
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
}
DB is what the statements need: a pgx pool, a connection or a transaction, all three of which satisfy it. Narrow on purpose — a handler that takes this cannot begin a transaction it was not given, and a test can hand it whatever it likes.
type Limits ¶
type Limits struct {
DefaultPageSize int
MaxPageSize int
MaxFilters int
MaxSortTerms int
// MaxOffset bounds how deep ?page may reach. Offset paging is the one
// dimension of a request whose cost grows with the number the client sent,
// and it is the dimension the exit is least able to soften: ?cursor did not
// come out, so a client walking a large collection has no cheaper spelling
// to be redirected to.
MaxOffset int
}
Limits are the resource's declared ceilings, emitted from the schema so the exit refuses the same oversized requests the API did.
type ListRequest ¶
ListRequest is a parsed list query.
type Options ¶
type Options struct {
Authors AuthorsHooks
Orgs OrgsHooks
Posts PostsHooks
}
Options carries the seams the handlers cannot supply for themselves.
In sqlb these were hooks on a registry; here they are function fields, which is the same seam with the machinery removed. A resource whose table declared Scoped or SoftDelete will not register until they are set — see Register.
type Org ¶
type Org struct {
ID string `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Slug string `db:"slug" json:"slug"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
}
Org a tenant. Every other table is scoped to one.
func GetOrg ¶
GetOrg reads one row by primary key. The extra conditions are whatever confines this table — a tenant, a soft delete — and they are part of the lookup rather than a check afterwards, so a row outside them is a 404 and not a 403 that confirms it exists.
func InsertOrg ¶
InsertOrg writes one row and reads it back, so database defaults and computed columns arrive without a second query.
The column order is the schema's, not the map's: a generated statement whose text depends on map iteration is a statement that cannot be diffed.
func ListOrg ¶
ListOrg reads a page. It returns one row more than asked for when there is one, which is how the handler answers has_more without a second count.
func UpdateOrg ¶
func UpdateOrg(ctx context.Context, db DB, id any, changes map[string]any, where []Condition) (Org, error)
UpdateOrg writes the named columns of one row and reads the row back. An empty change set is the caller's mistake rather than a statement with no SET clause, which Postgres will not parse.
type OrgsHooks ¶
type OrgsHooks struct {
// Confine narrows every statement this resource issues. Nil means
// unconfined, which is only allowed when the schema declared nothing.
Confine func(*http.Request) ([]Condition, error)
// Assign supplies column values a create must set that no request body
// carries. It runs before the insert and its values win.
Assign func(*http.Request) (map[string]any, error)
}
OrgsHooks are the seams for /orgs.
type Page ¶
type Page[T any] struct { Items []T `json:"items"` Page int `json:"page"` PerPage int `json:"per_page"` HasMore bool `json:"has_more"` Total *int64 `json:"total,omitempty"` }
Page is the body of a list response, and is the envelope sqlb served, minus next_cursor: keyset paging did not come out with the rest, so offering the field would be a promise this code cannot keep.
type Post ¶
type Post struct {
ID string `db:"id" json:"id"`
OrgID string `db:"org_id" json:"org_id"`
AuthorID string `db:"author_id" json:"author_id"`
Title string `db:"title" json:"title"`
Body string `db:"body" json:"body"`
Status string `db:"status" json:"status"`
ViewCount int64 `db:"view_count" json:"view_count"`
PublishedAt *time.Time `db:"published_at" json:"published_at"`
CreatedAt time.Time `db:"created_at" json:"created_at"`
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
DeletedAt *time.Time `db:"deleted_at" json:"deleted_at"`
}
Post a blog post.
func GetPost ¶
GetPost reads one row by primary key. The extra conditions are whatever confines this table — a tenant, a soft delete — and they are part of the lookup rather than a check afterwards, so a row outside them is a 404 and not a 403 that confirms it exists.
func InsertPost ¶
InsertPost writes one row and reads it back, so database defaults and computed columns arrive without a second query.
The column order is the schema's, not the map's: a generated statement whose text depends on map iteration is a statement that cannot be diffed.
func ListPost ¶
ListPost reads a page. It returns one row more than asked for when there is one, which is how the handler answers has_more without a second count.
func UpdatePost ¶
func UpdatePost(ctx context.Context, db DB, id any, changes map[string]any, where []Condition) (Post, error)
UpdatePost writes the named columns of one row and reads the row back. An empty change set is the caller's mistake rather than a statement with no SET clause, which Postgres will not parse.
type PostsHooks ¶
type PostsHooks struct {
// Confine narrows every statement this resource issues. Nil means
// unconfined, which is only allowed when the schema declared nothing.
Confine func(*http.Request) ([]Condition, error)
// Assign supplies column values a create must set that no request body
// carries. It runs before the insert and its values win.
Assign func(*http.Request) (map[string]any, error)
}
PostsHooks are the seams for /posts.
Confine is required here (deleted_at declares a soft delete), and returns the conditions every read and write is narrowed by — the predicate a BeforeQuery hook used to add.
type Problem ¶
type Problem struct {
Type string `json:"type,omitempty"`
Title string `json:"title,omitempty"`
Status int `json:"status,omitempty"`
Detail string `json:"detail,omitempty"`
Errors []*ProblemDetail `json:"errors,omitempty"`
}
Problem is the error body, RFC 9457 shaped — the same one sqlb served, so a client's error handling does not change on the way out.
type ProblemDetail ¶
type ProblemDetail struct {
Message string `json:"message"`
Location string `json:"location,omitempty"`
Allowed []string `json:"allowed,omitempty"`
}
ProblemDetail is one rejected parameter or field. Allowed carries what would have worked instead, which is the half of an error message that saves a round trip.