/** * ModelCollection — a list of ORM models that also carries the query total. * * What the ORM read queries (`where` / `select` / `find` filter-form / `all` / * `withTrashed`) return. It IS an Array — iterate it, index it, slice it, `.map` * it, `.filter` it, read `.length`, `JSON.stringify` it — so every existing * caller keeps working unchanged (`Array.isArray(coll) === true`). It adds one * thing: the TOTAL number of rows matching the query's filter, independent of * `limit` / `offset`. * * The total is free. Every one of those methods already runs the fetch COUNT * probe (`db.fetch` / `probeTotal`) that computes `SELECT COUNT(*)` for the same * filter; the ORM used to hydrate the page of models and throw that count away. * This class carries it instead, so a caller with 20 models on the page can * still learn there are 250 rows in the set. ZERO extra queries. * * Uniform across all four Tina4 frameworks (ADR-0064). Same concept, language- * idiomatic accessor name: * * Python / Ruby : get_total_records() to_paginate() * PHP / Node : getTotalRecords() toPaginate() * * The accessor is a METHOD, not a `.total` property, on purpose: `Array#count` * exists in Ruby and `list.count()` in Python, so a `.count` would shadow a * built-in. `DatabaseResult` keeps its `.count` property (it is not a list); both * expose the identical seven-key `toPaginate()` envelope. * * ### Why a real Array subclass with `[Symbol.species] === Array` * * `class X extends Array` is the parity-faithful backing (the ADR-0064 table * says Node is a "subclass of Array"), and it keeps `Array.isArray` true and * `instanceof ModelCollection` true (mirroring Python's `isinstance`). BUT a bare * Array subclass is a footgun: `map`/`filter`/`slice` build the result via * `this.constructor[Symbol.species]`, calling the constructor with a single * NUMBER (the length), and `new X(oneNumber)` means "length N", not "one * element". Overriding `[Symbol.species]` to return `Array` makes every derived * operation build a plain `Array` — so `.map`/`.filter`/`.slice`/spread never * touch this constructor and can never explode. The constructor below is also * defended against the numeric-length call directly, so even a path that ignores * species is safe. */ /** The canonical pagination envelope — seven snake_case keys (ADR-0043/0064). */ export interface PaginateEnvelope { records: unknown[]; total: number; page: number; per_page: number; total_pages: number; limit: number; offset: number; } export declare class ModelCollection extends Array { /** Total rows matching the query's filter (ignores limit/offset). */ private _total; /** The SQL limit that produced this page. */ private _limit; /** The SQL offset that produced this page. */ private _offset; /** * Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain * `Array`, never another `ModelCollection`. This is what defuses the Array- * subclass constructor/species trap: the engine never calls this constructor * with a length during those operations. */ static get [Symbol.species](): ArrayConstructor; /** * @param items the page of hydrated model instances (or the length, when the * engine constructs a derived array — defended against here). * @param total total rows matching the query's filter (ignores limit/offset). * @param limit the SQL limit that produced this page. * @param offset the SQL offset that produced this page. */ constructor(items?: readonly T[] | number, total?: number, limit?: number, offset?: number); /** * Total rows matching the query's filter, ignoring limit/offset. * * This is the whole point of the collection: the page slice you are iterating * is capped by `limit`, but this number is the full count of matching rows — * what a pager needs to render "page 3 of 13". */ getTotalRecords(): number; /** * The canonical pagination envelope — seven snake_case keys, identical to * `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks' * `toPaginate()` / `to_paginate()`. * * records the page's rows as plain objects (never re-sliced) * total getTotalRecords() — the true total for the filter * page floor(offset / per_page) + 1 * per_page the query's limit * total_pages ceil(total / per_page) * limit the SQL limit actually applied * offset the SQL offset actually applied * * `records` are model dicts (via `toDict()`, the same serialisation the * framework applies to a model in a JSON response), so the JSON a client sees * matches `DatabaseResult` exactly — the result is uniform whether the route * returned a raw `db.fetch()` or an ORM query. */ toPaginate(): PaginateEnvelope; }