New:Socket for Asana Is Now Available.Learn more
Get Started

@ultimat3/query

Package Overview
Dependencies
Maintainers
1
Versions
26
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ultimat3/query - npm Package Compare versions

Comparing version
16.0.0
to
17.0.0
+21
-1
CLAUDE.md

@@ -211,2 +211,7 @@ # @ultimat3/query

and a bundled app loses its error titles or throws `X_REGISTRAR_MISSING`. Never `false`.
- **Every numeric option is refused when it is not a FINITE number, `As of 2026-08-26`.**
`@ultimat3/core`'s `finiteOption()` guards the read cache's `ttlMs` and `search()`'s `page.max`,
`page.default` and `termMax`; `bun run finite-bounds` is the ratchet and this package is pinned
at **zero**. `??` guards nullish and `NaN` is not, so an unchecked bound is a comparison that
reads false forever — and `Math.max`/`Math.floor` propagate `NaN` rather than validating it.
- **`search()` is a FACTORY over `query()`, never a ninth primitive.** It owns the input schema

@@ -225,3 +230,18 @@ (`q` + a `limit` bounded in the schema, beside the read's own keys), trims and refuses a blank

on no page at all is the defect 12.0.0 spent a release removing from the timestamp seek; the
`fix:` names the entity chain, which pages this read correctly.
`fix:` names the entity chain, which pages this read correctly. **The refusal is on the rows that
would be CUT, never on the window**, `As of 2026-08-26`: a screen demanding `first >= limit`
refuses the framework's own default pair — `limit` defaults to 20 and `first` arrives from a
client — so every `pageSize` under 20 was a 500 at page ONE, including a search matching three
rows that fit the window twice over. `windowOf.execute` asserts on `rows.length` instead, so a
page that is whole is served with `hasNextPage: false` and no cursor is ever handed back that a
second call is guaranteed to refuse. **The one page it serves is the
chain's own ORDER**, `As of 2026-08-26`: `onePage.seek` narrows the window with `Builder.limit()`
and never with `Builder.seek()`, which sets `totalized` — `servedOrder()` is then `totalOrder([])`
= `id asc`, because the ranking lives inside the chain behind the row thunk and no `OrderKey` here
can name it, and `execute()` re-sorts the page the provider already ranked. Measured with a chain
serving `z, m, a`: `runQuery` answered `z, m, a` and `.page(input, { first: 2 })` answered `a, m`,
dropping the top-ranked row off page one. It exposes no `total()` for the same reason — an absent
`total` is how `SqlSource` says the source already serves one order it can be resumed in. The
fixture in `search.test.ts` must stay NOT id-ascending: `ROWS` is `a, b, c`, which is why the
shipped page test could not see this.
- Policy runs per subscriber for live queries. Never cache a decision across actors.

@@ -228,0 +248,0 @@ - The matcher patches from `QueryShape`, never from SQL text.

+6
-6
{
"name": "@ultimat3/query",
"version": "16.0.0",
"version": "17.0.0",
"description": "The query primitive: a policy-checked read, optionally live, with cursor pagination and an incremental matcher",

@@ -38,8 +38,8 @@ "license": "MIT",

"dependencies": {
"@ultimat3/cache": "16.0.0",
"@ultimat3/core": "16.0.0",
"@ultimat3/http": "16.0.0",
"@ultimat3/policy": "16.0.0",
"@ultimat3/schema": "16.0.0"
"@ultimat3/cache": "17.0.0",
"@ultimat3/core": "17.0.0",
"@ultimat3/http": "17.0.0",
"@ultimat3/policy": "17.0.0",
"@ultimat3/schema": "17.0.0"
}
}

@@ -13,2 +13,3 @@ /**

createContext,
finiteOption,
logger,

@@ -190,3 +191,9 @@ runWithContext,

? await readOnce(ctx, key, read)
: await readThrough(ctx, key, def.cache.ttlMs ?? DEFAULT_READ_CACHE_TTL_MS, read, tags);
: await readThrough(
ctx,
key,
finiteOption('the read cache', 'ttlMs', def.cache.ttlMs ?? DEFAULT_READ_CACHE_TTL_MS),
read,
tags,
);
}

@@ -193,0 +200,0 @@

@@ -9,3 +9,3 @@ /**

import { assert, type Ctx } from '@ultimat3/core';
import { assert, type Ctx, finiteOption } from '@ultimat3/core';
import type { InferOutput, Shape, Simplify } from '@ultimat3/schema';

@@ -17,3 +17,3 @@ import { t } from '@ultimat3/schema';

import type { SeekKey } from './shape';
import type { SqlSource, SqlText } from './source';
import type { Builder, SqlSource, SqlText } from './source';
import { from } from './source';

@@ -81,3 +81,4 @@

/**
* A search serves ONE page, and asking for a second is refused rather than answered wrongly.
* A search serves ONE page, in the order the chain served it, and asking for a second is refused
* rather than answered wrongly.
*

@@ -92,9 +93,38 @@ * The rows come from the entity chain, which pages by its own keyset cursor — proven against a real

* page with the entity chain's own `.search(term).after(cursor)`, or raise this read's `limit`.
*
* **The refusal is on the rows that would be CUT, never on the window** (`As of 2026-08-26`). It
* used to serve `first` rows, mint an `endCursor` and report `hasNextPage: true` — a connection
* protocol saying "call me again with this" for a call the cursor assert is guaranteed to throw on.
* A first repair demanded `first >= limit` and refused the window itself, which refuses the
* framework's OWN default pair: `limit` defaults to 20 and `first` arrives from a client
* (`{ first: query.pageSize }`), so every `pageSize` under 20 was a 500 at page ONE — including
* every search that matched three rows and fit the window twice over. A screen that fires on a
* request the read can answer completely is not protecting anyone.
*
* So the condition is the one thing that is actually wrong: the read answered MORE rows than the
* window carries, and this seam has no second page to put the rest on. Below that, the page is
* whole and `hasNextPage` is false by construction — the true stop signal, and no cursor is ever
* handed back that a second call is guaranteed to refuse.
*
* **`seek` narrows the WINDOW and never the ORDER, and that is the whole reason this wrapper takes a
* `Builder`.** `Builder.seek()` sets `totalized`, so `servedOrder()` becomes `totalOrder([])` —
* `id asc`, because the relevance ordering lives inside the chain behind the row thunk and no key
* here can name it — and `execute()` then re-sorts the page the provider already ranked. Measured
* with a chain serving `z, m, a`: `runQuery` answered `z, m, a` and `.page(input, { first: 2 })`
* answered `a, m`, dropping the top-ranked row off page one. `limit()` is the same window with no
* ordering claim attached, which is what an already-ranked page needs.
*/
const onePage = <Row extends object>(base: SqlSource<Row>, entity: string): SqlSource<Row> => ({
const onePage = <Row extends object>(
base: Builder<Row>,
entity: string,
/** Rows the chain was capped at — this read's `limit` input, and its whole page. */
served: number,
): SqlSource<Row> => ({
toSQL: (): SqlText => base.toSQL(),
execute: () => base.execute(),
shape: () => base.shape(),
...(base.total === undefined ? {} : { total: () => onePage(base.total?.() ?? base, entity) }),
seek: (after: SeekKey | null, limit: number): SqlSource<Row> => {
// No `total()`: `SqlSource` reads its absence as "the source already serves one order it can be
// resumed in", which is exactly true of a relevance ranking — and `total()` would totalize the
// Builder, re-sorting that ranking by id for the same reason `seek` must not.
seek: (after: SeekKey | null, window: number): SqlSource<Row> => {
assert(

@@ -105,3 +135,5 @@ after === null,

);
return onePage(base.seek?.(after, limit) ?? base, entity);
// `paginate` asks for `first + 1` — the extra row IS `hasNextPage` — so the window it names is
// one wider than the page it will serve.
return windowOf(base, entity, served, window - 1);
},

@@ -111,2 +143,39 @@ });

/**
* The one page, narrowed to the window `paginate` asked for and refused only when it does not FIT.
*
* The check is on the rows that came back, not on the two numbers, because those two numbers cannot
* decide it: `first: 10` against a `limit` of 20 is a complete answer whenever the term matched ten
* rows or fewer, which is most searches. Refusing it on the declaration alone made the framework's
* own defaults a 500.
*
* The `fix:` names `served` — the read's declared `limit` — and never the count that came back: a
* window sized to today's result set breaks on the first row added to the corpus.
*/
const windowOf = <Row extends object>(
base: Builder<Row>,
entity: string,
served: number,
/** Rows `paginate` will serve. It fetched one more, to decide `hasNextPage`. */
first: number,
): SqlSource<Row> => {
const windowed = base.limit(first + 1);
return {
toSQL: (): SqlText => windowed.toSQL(),
shape: () => windowed.shape(),
// No `seek()` and no `total()`: `paginate` seeks once, and `SqlSource` reads an absent `total`
// as "the source already serves one order it can be resumed in" — exactly true of a relevance
// ranking, which `total()` would re-sort by id for the same reason `seek` must not.
execute: async () => {
const rows = await windowed.execute();
assert(
rows.length <= first,
`search of ${entity} answered more than the ${String(first)} rows .page() asked for, and this read has no second page: the rest would be on no page at all`,
`read.page(input, { first: ${String(served)} }) # widen the window to the read's whole page — or narrow the page: read({ q, limit: ${String(first)} })`,
);
return rows;
},
};
};
/**
* The factory. `live: false` and the source declares itself unpatchable, because the incremental

@@ -120,7 +189,10 @@ * matcher decides membership from `QueryShape` filters and a `tsvector` match is not one of them —

const page = def.page ?? {};
const max = page.max ?? DEFAULT_PAGE_MAX;
const max = finiteOption('search()', 'page.max', page.max ?? DEFAULT_PAGE_MAX);
const shape = {
...((def.input ?? {}) as S),
q: termSchema(def.termMax ?? DEFAULT_TERM_MAX),
limit: limitSchema(max, Math.min(page.default ?? DEFAULT_PAGE_SIZE, max)),
q: termSchema(finiteOption('search()', 'termMax', def.termMax ?? DEFAULT_TERM_MAX)),
limit: limitSchema(
max,
Math.min(finiteOption('search()', 'page.default', page.default ?? DEFAULT_PAGE_SIZE), max),
),
} as SearchShape<S>;

@@ -149,5 +221,5 @@

const rows = () => chain.search(term).limit(parsed.limit).all();
return onePage(from<Row>(name, rows).raw('full-text search'), name);
return onePage(from<Row>(name, rows).raw('full-text search'), name, parsed.limit);
},
});
};