UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

279 lines (250 loc) 5.7 kB
/** * Sort direction for list queries. */ export type SortDirection = "asc" | "desc"; /** * Sort option for a whitelisted field. */ export type SortOption<Field extends string = string> = { /** * Field to sort by. */ field: Field; /** * Sort direction. */ direction: SortDirection; }; /** * Normalized offset pagination request. */ export interface OffsetPage { /** * Pagination mode. */ readonly kind: "offset"; /** * Page size after defaulting and max-limit clamping. */ readonly limit: number; /** * Zero-based item offset. */ readonly offset: number; } /** * Normalized cursor pagination request. */ export interface CursorPage { /** * Pagination mode. */ readonly kind: "cursor"; /** * Page size after defaulting and max-limit clamping. */ readonly limit: number; /** * Cursor for the current page, or null for the first page. */ readonly cursor: string | null; } /** * Normalized pagination request. */ export type Page = OffsetPage | CursorPage; /** * Offset pagination response metadata. */ export type OffsetPageInfo = OffsetPage & { /** * Total item count. */ readonly total: number; /** * Whether another page exists. */ readonly hasMore: boolean; }; /** * Cursor pagination response metadata. */ export type CursorPageInfo = CursorPage & { /** * Cursor for the next page, or null when there is no next page. */ readonly nextCursor: string | null; /** * Whether another page exists. */ readonly hasMore: boolean; }; /** * Pagination response metadata. */ export type PageInfo = OffsetPageInfo | CursorPageInfo; /** * Paginated list result. */ export interface PageResult<TItem, TPage extends PageInfo = PageInfo> { /** * Page items. Result helpers clone the input array. */ readonly items: TItem[]; /** * Page metadata. */ readonly page: TPage; } /** * Options for normalizing pagination input. */ export interface NormalizePageOptions { /** * Limit used when the caller does not provide one. */ readonly defaultLimit: number; /** * Maximum allowed limit. Larger caller values are clamped. */ readonly maxLimit: number; } /** * Raw offset pagination input. */ export interface OffsetPageInput { /** * Requested page size. */ readonly limit?: number | null; /** * Requested zero-based offset. */ readonly offset?: number | null; } /** * Raw cursor pagination input. */ export interface CursorPageInput { /** * Requested page size. */ readonly limit?: number | null; /** * Requested cursor. */ readonly cursor?: string | null; } /** * Error thrown when pagination input is invalid. */ export class PaginationError extends Error { constructor(message: string) { super(message); this.name = "PaginationError"; } } function assertPositiveInteger(name: string, value: number): void { if (!Number.isInteger(value) || value < 1) { throw new PaginationError(`${name} must be a positive integer.`); } } function normalizeLimit( input: number | null | undefined, options: NormalizePageOptions, ): number { assertPositiveInteger("defaultLimit", options.defaultLimit); assertPositiveInteger("maxLimit", options.maxLimit); if (options.defaultLimit > options.maxLimit) { throw new PaginationError( "defaultLimit must be less than or equal to maxLimit.", ); } if (input == null) return options.defaultLimit; if (!Number.isInteger(input) || input < 1) { throw new PaginationError("limit must be a positive integer."); } return Math.min(input, options.maxLimit); } /** * Normalize offset pagination input. * * The limit is defaulted and clamped to `maxLimit`; offset must be a * non-negative integer. */ export function normalizeOffsetPage( input: OffsetPageInput, options: NormalizePageOptions, ): OffsetPage { const limit = normalizeLimit(input.limit, options); const offset = input.offset ?? 0; if (!Number.isInteger(offset) || offset < 0) { throw new PaginationError("offset must be a non-negative integer."); } return { kind: "offset", limit, offset, }; } /** * Normalize cursor pagination input. * * The limit is defaulted and clamped to `maxLimit`; cursor must be a string or * null. */ export function normalizeCursorPage( input: CursorPageInput, options: NormalizePageOptions, ): CursorPage { const limit = normalizeLimit(input.limit, options); const cursor = input.cursor ?? null; if (cursor !== null && typeof cursor !== "string") { throw new PaginationError("cursor must be a string or null."); } return { kind: "cursor", limit, cursor, }; } /** * Create an offset page result and derive `hasMore`. */ export function offsetPageResult<TItem>( items: readonly TItem[], page: OffsetPage, total: number, ): PageResult<TItem, OffsetPageInfo> { if (!Number.isInteger(total) || total < 0) { throw new PaginationError("total must be a non-negative integer."); } return { items: [...items], page: { ...page, total, hasMore: page.offset + items.length < total, }, }; } /** * Create a cursor page result and derive `hasMore` from `nextCursor`. */ export function cursorPageResult<TItem>( items: readonly TItem[], page: CursorPage, nextCursor: string | null, ): PageResult<TItem, CursorPageInfo> { if (nextCursor !== null && typeof nextCursor !== "string") { throw new PaginationError("nextCursor must be a string or null."); } return { items: [...items], page: { ...page, nextCursor, hasMore: nextCursor !== null, }, }; }