findMany AST and server contract
How Athena.js findMany object selections compile, which gateway routes they depend on, and where typed inference comes from.
findMany(...) is Athena.js's canonical eager read surface for object-based selections.
const result = await athena.from("orchestral_sections").findMany({
select: {
name: true,
instruments: {
select: {
name: true,
},
},
},
where: {
active: true,
},
limit: 10,
});This page explains what that object tree means, how Athena.js sends it to the gateway, and what behavior is local to the SDK versus required from the server.
What "AST" means here
In this context, AST just means "object tree before transport".
This call:
await athena.from("orchestral_sections").findMany({
select: {
name: true,
instruments: {
select: {
name: true,
},
},
},
});starts as an object tree, then Athena.js compiles it into the existing select-string grammar:
"name,instruments(name)"That compiled form is why findMany(...) can be added without forcing a brand-new gateway route contract.
Default transport
By default, Athena.js compiles findMany(...) into the same gateway fetch fields the older read surface already uses:
selectbecomescolumnswherebecomesconditionsorderBybecomessort_bylimitstayslimit
Example:
await athena.from("orders").findMany({
select: {
id: true,
total: true,
},
where: {
customer_id: "cust_1",
total: { gte: 100 },
},
orderBy: {
created_at: "desc",
},
limit: 25,
});Compiles into the existing fetch-style payload shape:
{
"table_name": "orders",
"columns": "id,total",
"conditions": [
{
"operator": "eq",
"column": "customer_id",
"value": "cust_1"
},
{
"operator": "gte",
"column": "total",
"value": 100
}
],
"sort_by": {
"field": "created_at",
"direction": "descending"
},
"limit": 25
}Direct AST transport is not part of the default Athena server contract
Athena.js exposes query-shape types that can describe the original object tree, but the current Athena server release still treats the compiled fetch/query transport as canonical.
Important constraints:
- the normal compiled transport remains the default
- top-level direct AST
/gateway/fetchrequest bodies are not part of the current public Athena server contract - Athena.js still falls back to the compiled transport when a chain carries state that a direct AST body cannot represent losslessly
The exported query AST types
Athena.js exposes the types behind findMany(...):
interface AthenaRelationSelectNode<TSelect extends AthenaSelectShape = AthenaSelectShape> {
select: TSelect
as?: string
via?: string
}
type AthenaSelectShape = Record<string, true | AthenaRelationSelectNode<AthenaSelectShape>>
interface AthenaFindManyOptions<Row, TSelect extends AthenaSelectShape> {
select: TSelect
where?: AthenaWhere<Row>
orderBy?: AthenaOrderBy<Row>
limit?: number
}This is what powers:
- scalar field selection
- nested relation selection
- relation aliasing through
as - relation disambiguation through
via
Current server note:
- schema-qualified relation selectors combined with
viaare not supported in this Athena release and return400 VALIDATION_FAILED - relation
onshapes are also unsupported on the current public server contract
Typed behavior
findMany(...) improves readability on every path, but the typing is strongest when the builder comes from fromModel(...).
Plain from<Row>(...)
On a plain runtime builder:
- scalar fields infer from
Row - unresolved relation leaves can fall back to
unknown
Typed fromModel(...)
On a typed registry builder:
- relation names resolve through model metadata
- one-to-many and many-to-many relations become arrays
- one-to-one and many-to-one relations become
T | null viacan disambiguate when multiple joins are possible
Server routes findMany(...) depends on
Today, findMany(...) is built around existing gateway routes:
POST /gateway/fetchfor normal compiled readsPOST /gateway/queryfor the UUID text-comparison fallback path
That means no new gateway route is required just to support the default findMany(...) behavior.
UUID fallback
When Athena.js sees an identifier-like column with a UUID-looking equality comparison, it can fall back to /gateway/query so the comparison stays deterministic:
await athena.from("form_sessions").findMany({
select: {
session_id: true,
},
where: {
session_id: "550e8400-e29b-41d4-a716-446655440000",
},
});That route decision is SDK behavior, not a second public read API you need to call manually.
Local validation versus gateway failures
There are two failure modes to keep straight.
Local validation errors throw
If the findMany(...) input is structurally invalid, Athena.js throws before any request is sent.
Examples:
- empty
select - unsupported
where.notshapes that cannot compile losslessly
Gateway failures return AthenaResult.error
If the request reaches the gateway and fails there, Athena.js returns a normal AthenaResult<T> with a structured error object:
const result = await athena.from("missing_table").findMany({
select: {
id: true,
},
});
if (result.error) {
console.error(result.error.message);
console.error(result.error.status);
console.error(result.error.endpoint);
}What the server must support
For default findMany(...) support, the gateway only needs to preserve the compiled transport Athena.js already targets:
columnsconditionssort_bylimit- the nested select-string grammar for relation reads
For Athena 2.4.0 compatibility work, that includes schema-qualified base
tables such as public.chat_subscriptions and relation-side schema-qualified
select tokens such as users:athena.users(id,username,image).
If the server later wants direct AST transport, that can be added as a separate
capability. The current Athena.js design does not require that server work in
order to use findMany(...) today, and the current Athena server release does
not treat top-level AST fetch bodies as supported public input.