asobi_ops_params (asobi v0.75.1)
View SourceRequest-input parsing for the ops read plane: query string, and the id in a bound path segment.
Three rules, each of them load-bearing:
- Numbers are parsed safely and clamped.
?limit=abcmust never reachbinary_to_integer/1(abadargis a 500 and a free log-flood), and?limit=10000000must never reach the database. Parsing goes throughasobi_qs:integer/5, which already fails soft to a default. order_byis built from a per-endpoint allowlist of atoms and nothing else. A user-supplied string reachingorder_byis SQL injection, so an unknown sort field is rejected rather than ignored - silently falling back to a default would hide the mistake from the caller.- Every sort ends on a unique column. Offset pagination over a non-unique key
can return one row twice and skip another between pages, so
sort/3appends the primary key as a tie-breaker, andsort/4takes the unique column for a row set that is not keyed onid.
?page=N and ?offset=N both select a window. page wins when both are
given. The offset returned is the one the query actually used - see page/1.
Summary
Functions
Read a boolean filter parameter.
Decode an opaque ?cursor= token back to its keyset value.
Encode a keyset value as the opaque cursor token cursor/1 accepts.
Read an exact-match filter parameter.
Build an ILIKE pattern from the search parameter search/2 accepts.
Window for a list endpoint: a clamped limit and a clamped offset.
Read a free-text search parameter.
Resolve ?sort= and ?order= against a per-endpoint allowlist.
sort/3 for a row set whose unique key is not id.
Whether Id is a canonical lowercase hyphenated uuid.
Types
-type page_spec() :: #{limit := pos_integer(), offset := non_neg_integer()}.
-type sort_spec() :: [{atom(), asc | desc}].
-type tie_break() :: {atom(), asc | desc}.
Functions
Read a boolean filter parameter.
Only true and false are values. ?active=1 is a typo, and reading it as
false would answer with the exact opposite of what was asked for, so
anything else is none and the filter does not apply.
Decode an opaque ?cursor= token back to its keyset value.
Cursors are base64url without padding so they survive a query string intact and carry no meaning a caller can hand-craft. Anything that is not a valid token is an error, never a silent fall back to the first page.
Encode a keyset value as the opaque cursor token cursor/1 accepts.
Read an exact-match filter parameter.
none when the parameter is absent, empty, valueless, or longer than 64
bytes - the same soft drop search/2 applies, and for the same reason: a
value no column of this size can hold is not worth a query.
Build an ILIKE pattern from the search parameter search/2 accepts.
Window for a list endpoint: a clamped limit and a clamped offset.
limit defaults to 50 and is clamped to [1, 200]. offset is clamped to
[0, 100000] - a deeper offset is a sequential scan, not a page.
The offset is then snapped down to a whole multiple of the limit, because
kura_paginator:paginate/3 is page-based and can only express offsets that
land on a page boundary. Snapping keeps the offset reported in the envelope
equal to the offset the query ran with, rather than echoing one the caller
asked for and did not get.
Read a free-text search parameter.
Returns none when the parameter is absent, empty, valueless, or longer than
64 bytes. The one place the length rule lives, so a search that reaches the
database and a search matched in memory accept the same input.
-spec sort(params(), sort_allowlist(), sort_spec()) -> {ok, sort_spec()} | {error, {unknown_sort, binary()} | {unknown_order, binary()}}.
Resolve ?sort= and ?order= against a per-endpoint allowlist.
Allowed maps the wire name to the column atom; only atoms already in that
list can reach the query. An unknown field or direction is an error, so the
caller answers 400 instead of quietly returning differently-ordered rows.
Default is used when no sort is requested. Either way the result is
completed with {id, desc} so the ordering is total.
-spec sort(params(), sort_allowlist(), sort_spec(), tie_break()) -> {ok, sort_spec()} | {error, {unknown_sort, binary()} | {unknown_order, binary()}}.
sort/3 for a row set whose unique key is not id.
A board's entries are unique on player_id within the board, a queue has one
row per mode: those are the columns the order has to end on there. Passing
the wrong one is not a syntax error, it is a page that can repeat a row, so
the tie-breaker is named by the endpoint rather than assumed.
Whether Id is a canonical lowercase hyphenated uuid.
Every primary key in this plane is a uuid column. Postgres raises on a
value that is not one, so an id checked only by the database turns a
malformed request into a 500 and a log line. Checking the shape first makes
it the 400 it is, and costs no query.