v0.53.0
A collection read says where the collection ends
list returns a page. The result is still an array, so it maps, spreads, and
iterates exactly as before, and it now carries hasMore and nextCursor beside
the rows. Pass nextCursor back as cursor, keeping where and orderBy the
same, to walk the rest:
let cursor: string | null = null;
const open = [];
do {
const page = await ablo.weatherReports.list({
where: { status: ['draft', 'review'] },
orderBy: { createdAt: 'asc' },
limit: 100,
...(cursor ? { cursor } : {}),
});
open.push(...page);
cursor = page.hasMore ? page.nextCursor : null;
} while (cursor);
A list read has always been a page: the server applies a default size and caps
the largest one. Until now that page state was dropped on arrival, so a read
that returned twenty of five hundred matching rows looked exactly like a
complete one. Check hasMore before treating a result as the whole set.
The live client keeps a local graph and loads a working set rather than pages, so
it rejects cursor instead of returning the first page again. Narrow the
where, or construct the client with transport: 'http' to page. On the live
client hasMore reports whether a limit cut the working set short, and
nextCursor is null.
GET /v1/projects returns the same list envelope as every other collection,
with has_more and next_cursor beside data.
The page cursor is called cursor
The parameter that resumes a collection is cursor, in the SDK and on every
HTTP collection route. It was starting_after, a spelling whose established
meaning elsewhere is a row id, while this value has always been an opaque token
tied to the sort it was issued for. A caller who read the familiar name and
passed a row id was refused, so the name promised something it never did.
starting_after is still accepted on the wire and is removed in a later
release. Requests that send it keep working; new code should send cursor.
Sending both uses cursor. The MCP list_records tool and the OpenAPI
description take cursor, and the spec marks the old name deprecated.
A filter reaches the server intact
where accepts operators as well as equality. An array value is an IN, and
tuple form spells the rest out:
const storms = await ablo.weatherReports.list({
where: [
['title', 'ILIKE', '%storm%'],
['createdAt', '>=', cutoff],
['status', 'IN', ['draft', 'review']],
],
});
Clauses combine with AND. For OR, run two reads and union the results.
On the stateless client (transport: 'http') an IN filter and every
tuple-form clause were previously discarded before the request left, and the
read came back unfiltered. An agent or worker that filtered a collection over
HTTP was reading more rows than it asked for, with nothing to indicate it. Every
transport now encodes a filter the same way.
A filter on a boolean field could also match the opposite rows rather than fail, when its value arrived as the database’s own text spelling. Boolean values are coerced before binding, and read back the same way.
A number field reads back as a number
A field declared as a number arrives as one whatever integer width its column uses. A wide column previously came back as a decimal string while its narrower neighbour came back as a number, so the type a caller received depended on a database detail the schema had already settled.
A stored value beyond the range a JavaScript number represents exactly now fails
with column_value_out_of_range rather than arriving quietly rounded. Declare
such a field as text to read those values digit for digit.
A reconnect cannot roll back a confirmed write
Each row in the live client records the log position it reflects. A bootstrap or
an on-demand read from an earlier position is left unapplied, so a snapshot that
arrives late no longer overwrites a row the client already knows to be newer.
The ordered change stream continues to carry every other writer’s edits. A
plugin receives that position as syncId on AppliedChange.
The base URL is checked where the credential travels
baseURL accepts an HTTPS origin, preserving a path prefix for a deployment
mounted under one, and plain HTTP for localhost. A URL that embeds its own
credentials, or carries a query or a fragment, is refused when the client is
constructed rather than failing later as an opaque request error. Every request
attaches the resolved key against this origin, so the rule lives beside the
option rather than in each application that sets it.
normalizeAbloHostedBaseUrl is now normalizeAbloBaseUrl. The old name
resolves to the same function and is removed in 0.54.0.
Two error codes added
organization_disabled is returned when an operator has disabled an
organization, and query_relation_expansion_too_large when a requested relation
expansion exceeds the nested-row budget. The error contract version is
2026-08-15.
CLI
Where a command sends a management key is resolved and checked in one place: an
explicit --url on the commands that take one, then ABLO_API_URL, then the
hosted default. A host given without a scheme becomes absolute, and a
destination that would put the key on the wire in clear, or one carrying its own
credentials, is refused before the request is made.