Skip to content

@btravstack/contract


@btravstack/contract / index

index ​

Type Aliases ​

Authenticated ​

ts
type Authenticated<T, R> = T & object;

Defined in: auth.ts:31

A contract node whose procedures require an authenticated caller.

Type Parameters ​

Type Parameter
T
R extends Requirements

Type Declaration ​

NameTypeDefined in
[PRINCIPAL]Rauth.ts:31

CursorRefused ​

ts
type CursorRefused = object;

Defined in: pagination.ts:208

A cursor that cannot be honoured under the sort it arrived with, and why: "malformed" never had a sort-shaped head to compare, where "sort-mismatch" did, and named a different one. The two are separately triageable — the first is not actionable, the second tells a caller to re-issue from the first page — so an adapter needs the reason, not just the refusal.

Properties ​

PropertyModifierTypeDefined in
cursorreadonlystringpagination.ts:210
reasonreadonly"malformed" | "sort-mismatch"pagination.ts:211
resumablereadonlyfalsepagination.ts:209

IsMarked ​

ts
type IsMarked<T> = T extends object ? true : false;

Defined in: auth.ts:37

Whether this exact node carries the marker.

Type Parameters ​

Type Parameter
T

Keyset ​

ts
type Keyset = object;

Defined in: pagination.ts:150

One keyset window: what to ask the store for, and how to fold what comes back.

The two halves ride one object because they have to AGREE. take is one more row than the page so the extra row answers "is there another page" without a second count query — and page subtracts that same row back out. A store queried for limit and folded as though it had been queried for limit + 1 reports the last page as having a next one, forever.

Properties ​

PropertyModifierTypeDescriptionDefined in
backwardreadonlybooleanWalk descending from the cursor. before pages backward; everything else forward.pagination.ts:154
cursorreadonlystring | undefinedThe cursor to resume from, whichever side it came from, or undefined at the end.pagination.ts:156
pagereadonly<T, U>(rows, cursorOf, item?) => Page<U>The rows the store answered — in the order it answered them — folded into a page, with cursorOf minting the opaque cursor for a row. item is there because the row a store seeks by is rarely the thing a port hands back: an adapter pages on a surrogate key and answers domain entities, so the cursor and the item come off the same row by two different routes. It defaults to the row itself.pagination.ts:166
takereadonlynumberHow many rows to ask for: the page, plus one to detect the next.pagination.ts:152

OneScheme ​

ts
type OneScheme<Q> = SeveralKeys<keyof Q> extends false ? Q : never;

Defined in: auth.ts:25

A requirement naming two schemes is OpenAPI's AND, and nothing here models it: @btravstack/http-server takes the first entry that satisfies, which is OR — so a two-key requirement copied out of an OpenAPI document would execute as a WEAKER rule than the one it states. Refused at the mark instead.

Type Parameters ​

Type Parameter
Q

Page ​

ts
type Page<T> = object & (
  | {
  hasPreviousPage: true;
  previousCursor: string;
}
  | {
  hasPreviousPage: false;
  previousCursor?: never;
}) & (
  | {
  hasNextPage: true;
  nextCursor: string;
}
  | {
  hasNextPage: false;
  nextCursor?: never;
});

Defined in: pagination.ts:15

One page of a listing, in the vocabulary a client and a server share.

A flag and its cursor are one fact, spelled once. hasNextPage: true carries the nextCursor that continues the listing; hasNextPage: false has no nextCursor field at all. So "there is more, and nothing to follow it with" — and its twin, a cursor nobody may use — are unrepresentable rather than merely unexpected, and a reader that checks the flag has the cursor in hand, with no null to widen it.

A cursor is an opaque string: the server's to mint and to read, the client's to hand back verbatim. Nothing above the adapter that issued one may interpret it.

Type Parameters ​

Type Parameter
T

Type Declaration ​

NameTypeDefined in
itemsreadonly T[]pagination.ts:15

PageQuery ​

ts
type PageQuery = object & (
  | {
  sort: Sort;
}
  | {
  sort?: never;
});

Defined in: pagination.ts:93

The flat shape a validated page input arrives in, before its two cursors have been narrowed to the one direction PageRequest allows.

A schema states "at most one of these" as a rule over two optional fields; a type states it as a union. This is the former, and pageRequest is the crossing between them.

The sort keeps PageRequest's two states and gains no third: a query either carries one or has no such field. An OPTIONAL sort would cross into the unsorted PageRequest at compile time and mint a sorted cursor at runtime, so it is refused here rather than at the seek.

Type Declaration ​

NameTypeDefined in
after?stringpagination.ts:95
before?stringpagination.ts:96
limitnumberpagination.ts:94

PageRequest ​

ts
type PageRequest<F> = (
  | {
  after?: string;
  before?: never;
  limit: number;
}
  | {
  after?: never;
  before?: string;
  limit: number;
}) & ([F] extends [never] ? object : object);

Defined in: pagination.ts:74

What a caller asks for: a size, at most one cursor, and — for a listing that declares one — the sort it runs in.

after and before are the opaque cursors a previous page handed back, and they are mutually exclusive in the type: a page runs in one direction, and "after X and before Y" is a range query wearing a page's clothes. A union is what makes that unrepresentable rather than merely documented.

F is the listing's own sortable vocabulary. There are two states and no third: a listing that names one always has a sort, a listing that names none can never carry one — which is what lets a cursor's arity be known from the request alone.

Type Parameters ​

Type ParameterDefault type
F extends stringnever

PrincipalKey ​

ts
type PrincipalKey = typeof PRINCIPAL;

Defined in: auth.ts:34

The marker's key, so a consumer's mapped type can Exclude it from keyof.


Requirement ​

ts
type Requirement = Readonly<Record<string, readonly string[]>>;

Defined in: auth.ts:13

One OpenAPI security requirement: a scheme, and the scopes it must grant.

The CARRIER — what a marked node holds and isAuthenticated reads back — so it says nothing about arity. OneScheme below is what refuses a second key.


Requirements ​

ts
type Requirements = readonly Requirement[];

Defined in: auth.ts:28

Requirements are ORed, in order: the first one a caller satisfies wins.


RequirementsOf ​

ts
type RequirementsOf<T> = T extends object ? R : never;

Defined in: auth.ts:40

What this exact node requires, or never when it is unmarked.

Type Parameters ​

Type Parameter
T

Sort ​

ts
type Sort<F> = object;

Defined in: pagination.ts:55

One sort key and the direction it runs in.

Both halves are required, so a field without a direction is unrepresentable. One key, not a list: a cursor has to encode every ordering column, and each extra column is another value the seek and the token must agree about.

Type Parameters ​

Type ParameterDefault type
F extends stringstring

Properties ​

PropertyModifierTypeDefined in
directionreadonly"asc" | "desc"pagination.ts:57
fieldreadonlyFpagination.ts:56

SortedKeyset ​

ts
type SortedKeyset<F> = object;

Defined in: pagination.ts:187

A keyset for a sorted listing, or the refusal of a cursor that was issued under a different one.

cursor carries BOTH values a sorted seek needs — the sort key's and the tiebreak's — because a store queried with only the first seeks on one column and silently skips every row that ties on it.

It is the DECODED pair, with the sort head already stripped and the escaping undone: it is what the seek takes, not what the client sent. So an adapter that refuses it downstream — a value its own storage cannot read — reports the adapter's vocabulary rather than the wire token, and a support engineer handed that string cannot replay the request with it.

Type Parameters ​

Type Parameter
F extends string

Properties ​

PropertyModifierTypeDefined in
backwardreadonlybooleanpagination.ts:190
cursorreadonlyreadonly [sortValue: string, key: string] | undefinedpagination.ts:192
pagereadonly<T, U>(rows, cursorOf, item?) => Page<U>pagination.ts:193
resumablereadonlytruepagination.ts:188
sortreadonlySort<F>pagination.ts:191
takereadonlynumberpagination.ts:189

SortIsDecided ​

ts
type SortIsDecided<Q> = "sort" extends keyof Q ? Q extends object ? unknown : object : unknown;

Defined in: pagination.ts:110

The gate pageRequest intersects into its parameter, refusing a sort that is neither required nor absent.

PageQuery's union already states the two states a sort may be in, but a union's arms are compared one at a time, and only that per-arm comparison consults the exactOptionalPropertyTypes relation — assignability to the union as a whole does not. So sort?: Sort, a key present but optional, still types as assignable to a PageQuery the union alone was meant to refuse. This gate closes that spelling directly.

Type Parameters ​

Type Parameter
Q

Functions ​

authenticated() ​

ts
function authenticated<R>(...requirements): <T>(node) => Authenticated<T, R>;

Defined in: auth.ts:71

Marks a contract node as requiring an authenticated caller, with OpenAPI's own requirement shape — a scheme and the scopes it must grant, ORed in the order given.

ts
export const contract = {
  orders: authenticated({ user: [] })({ place, find }),
  exports: authenticated({ user: ["orders:export"] }, { service: [] })(csvProcedure),
};

Applied to a record it is the default for every procedure beneath it; applied to a procedure it replaces that default for itself. Nearest mark wins. Returns the node unchanged and applies after a builder chain, never inside one. See packages/contract/AGENTS.md.

Type Parameters ​

Type Parameter
R extends Requirements & { readonly [I in string | number | symbol]: OneScheme<R[I]> }

Parameters ​

ParameterType
...requirementsR

Returns ​

<T>(node) => Authenticated<T, R>


isAuthenticated() ​

ts
function isAuthenticated(node): Requirements | undefined;

Defined in: auth.ts:84

What this exact node requires, or undefined when nobody marked it. Ancestry is the caller's to carry — @btravstack/http-server's routerOf walks the tree and passes the nearest mark down.

Parameters ​

ParameterType
nodeobject

Returns ​

Requirements | undefined


keyset() ​

Call Signature ​

ts
function keyset(request): Keyset;

Defined in: pagination.ts:318

The keyset pagination an adapter would otherwise hand-roll: the over-fetch, the direction, the trim, and both cursors.

It does not run the query — the store's own seek call is the one thing this tier cannot express, and every store spells it differently. What it owns is the arithmetic around that call, which is identical everywhere and is where the off-by-ones live.

Backward is not forward reversed. before walks the index descending, so the rows arrive newest-first and page hands them back ascending — a previous page reads the way the next one does. It also flips which side the extra row proves: paging forward, one more row means there is a page AFTER; paging backward it means there is one BEFORE.

A cursor is minted only from a row that is actually on the page, so an empty page carries neither — which is what makes both flags on Page honest.

A sorted request answers a union the caller must branch on. The cursor a sorted listing mints carries the sort it was issued under, so one replayed under a different field — or the same field in the other direction — is REFUSED rather than served from the wrong side.

Parameters ​
ParameterType
requestPageRequest
Returns ​

Keyset

Example ​
ts
const keys = keyset(request);
const rows = await store.seek(keys.cursor, keys.backward).take(keys.take);
return keys.page(rows, (row) => String(row.id));

Call Signature ​

ts
function keyset<F>(request): 
  | SortedKeyset<F>
  | CursorRefused;

Defined in: pagination.ts:319

The keyset pagination an adapter would otherwise hand-roll: the over-fetch, the direction, the trim, and both cursors.

It does not run the query — the store's own seek call is the one thing this tier cannot express, and every store spells it differently. What it owns is the arithmetic around that call, which is identical everywhere and is where the off-by-ones live.

Backward is not forward reversed. before walks the index descending, so the rows arrive newest-first and page hands them back ascending — a previous page reads the way the next one does. It also flips which side the extra row proves: paging forward, one more row means there is a page AFTER; paging backward it means there is one BEFORE.

A cursor is minted only from a row that is actually on the page, so an empty page carries neither — which is what makes both flags on Page honest.

A sorted request answers a union the caller must branch on. The cursor a sorted listing mints carries the sort it was issued under, so one replayed under a different field — or the same field in the other direction — is REFUSED rather than served from the wrong side.

Type Parameters ​
Type Parameter
F extends string
Parameters ​
ParameterType
requestPageRequest<F>
Returns ​

| SortedKeyset<F> | CursorRefused

Example ​
ts
const keys = keyset(request);
const rows = await store.seek(keys.cursor, keys.backward).take(keys.take);
return keys.page(rows, (row) => String(row.id));

page() ​

ts
function page<T>(items, cursors): Page<T>;

Defined in: pagination.ts:34

A page from its items and the cursor on each side, null where there is nothing to follow.

The flags are DERIVED rather than given: a cursor is what a caller needs to ask for the page on that side, so a side with no cursor is a side it cannot reach — which is what the flags say. An adapter whose pagination library reports a page the other way round, a hasPreviousPage: true with no cursor to go back with, therefore reports the reachable answer instead.

Type Parameters ​

Type Parameter
T

Parameters ​

ParameterType
itemsreadonly T[]
cursors{ next: string | null; previous: string | null; }
cursors.nextstring | null
cursors.previousstring | null

Returns ​

Page<T>


pageRequest() ​

ts
function pageRequest<Q>(query): (Q extends object ? PageRequest<F> : PageRequest<never>) & Omit<Q, "after" | "before">;

Defined in: pagination.ts:129

A validated page input, narrowed into the one-direction PageRequest a port takes, carrying any filters alongside it untouched.

before wins when both are somehow present. That precedence is unreachable through pageRequestOf, whose schema refuses the pair — it exists so this function is total rather than partial, not as a policy a caller should rely on.

A sort stays a sort: it is a field of the query like any filter, so it rides through untouched and the result is the PageRequest of that listing's own vocabulary rather than of none.

Type Parameters ​

Type Parameter
Q extends PageQuery

Parameters ​

ParameterType
queryQ & SortIsDecided<Q>

Returns ​

(Q extends object ? PageRequest<F> : PageRequest<never>) & Omit<Q, "after" | "before">

Released under the MIT License.