import type { StapelClient } from "@stapel/core"; import type { Category, CategoryChild, CategoryFeaturesResult, CategoryListParams, CategoryPage, CategoryTreeNode, CategoryTreeParams, MaxRevision } from "./types.js"; /** Options `createCategoriesApi` needs beyond the JSON client, forwarded from * the runtime — see `api/featuresRaw.ts` for why `features()` needs its own * transport at all. */ export interface CategoriesApiOptions { readonly fetch?: typeof globalThis.fetch; readonly credentials?: RequestCredentials; readonly defaultHeaders?: Record; } /** * The pair's typed operation surface — one method per stapel-categories * endpoint a storefront may call, bound to the injected {@link StapelClient} * (the per-module override seam of frontend-standard §7.2). Paths are relative * to the runtime's `baseUrl` (`/categories/api/v1/`). * * ── Five reads, and the eighteen operations that are NOT here ────────────── * * The contract has 23 paths. Every one of them is on `ReadOnlyOrStaff`, which * means SAFE methods are open to anonymous callers and everything else needs * `is_staff` — so the split below is not a taste call, it is the permission * boundary drawn on the surface: * * - **Public reads, and this pair's whole surface**: the category list (the * delta-sync source), `children`, `carousel`, `{id}/features`, and * `revision`. * - **Staff writes**: create / update / delete, `bulk_add`, * `bulk-commands`, `undelete`, `convert-type`, and the four * `feature-editor*` operations. These are the catalogue admin, which the * storefront wave leaves to Django admin (spec §4.3) — a pair that exposed * them would invite a public screen to call something only staff may. * - **`POST {id}/validate-dto/` is a WRITE in DRF's eyes**, so despite * reading like a public helper it answers 403 to a visitor. The listing * compose flow validates against the same rules with * `@stapel/attributes-react`'s client-side mirror plus the server's verdict * on publish; asking this endpoint would fail for exactly the people who * need it. * - **`GET {id}/validate-configs/`** is a catalogue-health read for an * operator, not a storefront read. * - **`GET translation-keys/`** is `IsServiceRequest` (service-to-service): * it is the extraction feed for translators, and it returns KEYS with * admin refs — never resolved labels. See `catalog/labels.ts`. * - **`GET data.json/`** requires a `?revision=` cache-buster and hands back * the whole table in one response. It is a legitimate alternative to the * paged walk, but it is a *second* sync protocol, and the module documents * the paged one. One protocol, tested, beats two, half-tested. * - The parallel `/features/` collection (list, retrieve, create, …) is the * feature tree in its own right — an admin axis. A storefront reads * features THROUGH a category, which is the only place inheritance and * order are resolved. * * Nothing is hidden: `manifest.json` lists the whole contract. * * These operations will be GENERATED from schema.json operationIds by gen-api * v2 (task `core-typed-ops`); until then they are hand-authored here (the ONE * legal home of path strings — `stapel/no-string-paths` §2.3 carve-out). */ export interface CategoriesApi { readonly client: StapelClient; /** * One page of the revision-paginated category list — the source the whole * tree is assembled from, and the only endpoint that reports deletions. * * Rows are ordered by `revision`, NOT by tree position: this is a sync feed, * not a menu. `revisions.global_max` is the cursor to store; * `revisions.deleted_ids` is the authoritative tombstone list for a delta * (see `catalog/sync.ts` for why it beats scanning `deleted: true` rows). */ list(params?: CategoryListParams, options?: { readonly signal?: AbortSignal; }): Promise; /** * One category row by id. * * The cheapest question in the contract, and the one that makes a * SERVER-DRIVEN tree walk possible at all: `tn_ancestors_pks` on the answer * is the whole chain from a root down to this row, so a deep link can be * turned into a ladder of `children` reads without transferring a catalogue. * * It takes an ID, not a slug — `lookup_field` is never overridden and the * list endpoint has no slug filter, which is why a slug still costs the * whole tree (see MODULE.md's upstream asks). */ retrieve(id: number, options?: { readonly signal?: AbortSignal; }): Promise; /** * The level below one category, in the order a storefront draws it. * * Redundant with the synced tree by construction, and deliberately kept: a * host that wants one branch without paying for the catalogue (an SSR * category page, a lazily-expanded admin-ish picker) has an endpoint for it, * and it is the one place the server does the `deleted` filtering. * * Since stapel-categories 0.22.0 the answer is a {@link CategoryChild} and * not a row: a POINTER into another branch arrives here at the `order` its * operator gave it, and an expanded branch answers with VALUES that have no * row at all. Both are still ordered by the server, so nothing on this side * re-sorts the array. */ children(id: number, options?: { readonly signal?: AbortSignal; }): Promise; /** * The carousel strip: `active` AND `carousel_enabled`, `tn_priority` * descending, cached server-side and sent with * `Cache-Control: public, max-age` — so this one is safe to call on every * landing render. */ carousel(options?: { readonly signal?: AbortSignal; }): Promise; /** * A category's resolved feature schema: its own features plus every ancestor's, * deduplicated by SLUG (an `inherit` override is a new row sharing the * parent's slug, and the version nearest this category wins), ordered by the * category's own `CategoryFeature.order` first. * * This is what the listing compose form draws with * `@stapel/attributes-react` and what labels a search facet. `config` * arrives verbatim — defaults are the reader's job, and attributes-react * owns them. * * Since stapel-categories 0.20.1 a `chips` parent with no features of its * own answers the EFFECTIVE schema instead — the intersection of its * children's, `divergent: true` beside a feature they disagree on — and * says which schema it sent via `X-Effective-From`. That header rides * outside `StapelClient`'s reach, so this one read goes over the raw * carve-out in `api/featuresRaw.ts` rather than `client.get`. */ features(id: number, options?: { readonly signal?: AbortSignal; }): Promise; /** * The table's current maximum revision, without transferring any rows. * * The cheap freshness probe: a stored snapshot whose cursor already equals * this needs no delta request at all. */ revision(options?: { readonly signal?: AbortSignal; }): Promise; /** * The first `depth` levels of the tree, NESTED, active rows only, ordered — * the one read a mega-menu makes, and the only endpoint that answers a whole * subtree in one round trip. * * It is not a cheaper `list()`: it carries four fields per node and no sync * bookkeeping at all, so it can neither seed nor advance the catalogue * snapshot. A menu wants three levels of names and pictures; the delta sync * wants every row and its revision. Two questions, two reads. */ tree(params?: CategoryTreeParams, options?: { readonly signal?: AbortSignal; }): Promise; } /** * The wire query object for one list request. * * Exported because the query KEY is built from it, so the key and the request * are the same value by construction. * * `include_deleted` is sent ONLY when the caller asked for a value, and the * server's own default is `true`. Sending it always would be harmless; leaving * it out when unasked keeps the cache key of a plain read from depending on * this pair's opinion about a server default. */ export declare function categoryListParams(params?: CategoryListParams): Record; export declare function createCategoriesApi(client: StapelClient, options?: CategoriesApiOptions): CategoriesApi; //# sourceMappingURL=categoriesApi.d.ts.map