Gamend.Query (gamend_core v1.0.1296)

Copy Markdown View Source

Query-building pieces shared by the contexts. Right now: paging.

Why this exists

Seven contexts each carried a private paginate/2, in four different behaviours. Three (quests, economy, inventory) read :page_size straight out of the options and applied it, so list_ledger(page_size: 1_000_000) was a single unbounded read. Two (groups, lobbies) clamped — to a hard-coded @max_page_size 1000 that ignored the configurable max_page_size limit entirely. Two (matchmaking, ready_checks) clamped correctly through Gamend.Limits.

GamendWeb.Pagination already fixed the same drift one layer up, and mix gamend.api.lint keeps controllers honest — but nothing was watching the contexts, and a plugin calls them directly. Both functions here clamp through Gamend.Limits, so the configured maximum is the maximum wherever the call came from.

Which one

page/2 always windows: the caller is paging, and no :page just means the first one. maybe_page/2 windows only when asked, and otherwise bounds the read at unpaginated_limit/0 — for a listing whose callers legitimately want "all of them", where "all" must still not mean an unbounded Repo.all over a growing table.

Summary

Functions

The rows whose field is exactly the id value.

Narrows a query over a table with a user_id column to one user, found by exact id or by a substring of their username or display name — so an admin can filter a list without knowing the raw id.

Applies the window when :page is given, and otherwise caps the read at unpaginated_limit/0.

Applies :page and :page_size, both clamped, defaulting to the first page.

The ceiling applied to a read that requested no page.

Functions

filter_id(query, field, value)

@spec filter_id(Ecto.Queryable.t(), atom(), String.t() | nil) :: Ecto.Queryable.t()

The rows whose field is exactly the id value.

For a caller holding an id rather than a search term -- a plugin scoping a read by user or lobby, a link from another page. nil and "" leave the query alone. A value that is not a UUID matches nothing: it arrives in request params, where comparing it raised Ecto.Query.CastError, and ignoring it instead would answer every row to a filter plainly meant to narrow them. KV did the first and push the second.

See filter_user/2 for the admin search that also matches names.

filter_user(query, value)

@spec filter_user(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Queryable.t()

Narrows a query over a table with a user_id column to one user, found by exact id or by a substring of their username or display name — so an admin can filter a list without knowing the raw id.

nil leaves the query alone. Economy, inventory and quests each carried an identical private copy of this.

maybe_page(query, opts)

@spec maybe_page(Ecto.Queryable.t(), keyword()) :: Ecto.Query.t()

Applies the window when :page is given, and otherwise caps the read at unpaginated_limit/0.

page(query, opts)

@spec page(Ecto.Queryable.t(), keyword()) :: Ecto.Query.t()

Applies :page and :page_size, both clamped, defaulting to the first page.

Query.page(query, page: 2, page_size: 50)

unpaginated_limit()

@spec unpaginated_limit() :: pos_integer()

The ceiling applied to a read that requested no page.