tempest-react-sdk 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -28,6 +28,7 @@ import { ForwardRefExoticComponent } from 'react';
28
28
  import { HashRouter } from 'react-router-dom';
29
29
  import { HTMLAttributes } from 'react';
30
30
  import { ImgHTMLAttributes } from 'react';
31
+ import { InfiniteData } from '@tanstack/react-query';
31
32
  import { InputEventHandler } from 'react';
32
33
  import { InputHTMLAttributes } from 'react';
33
34
  import { JSX } from 'react/jsx-runtime';
@@ -47,6 +48,7 @@ import { Path } from 'react-hook-form';
47
48
  import { PersistOptions } from 'zustand/middleware';
48
49
  import { PointerEventHandler } from 'react';
49
50
  import { QueryClient } from '@tanstack/react-query';
51
+ import { QueryKey } from '@tanstack/react-query';
50
52
  import { ReactElement } from 'react';
51
53
  import { ReactEventHandler } from 'react';
52
54
  import { ReactNode } from 'react';
@@ -77,10 +79,12 @@ import { UseFormProps } from 'react-hook-form';
77
79
  import { UseFormRegister } from 'react-hook-form';
78
80
  import { UseFormReturn } from 'react-hook-form';
79
81
  import { useFormState } from 'react-hook-form';
82
+ import { UseInfiniteQueryOptions } from '@tanstack/react-query';
80
83
  import { useLocation } from 'react-router-dom';
81
84
  import { useMatch } from 'react-router-dom';
82
85
  import { useNavigate } from 'react-router-dom';
83
86
  import { useParams } from 'react-router-dom';
87
+ import { UseQueryOptions } from '@tanstack/react-query';
84
88
  import { useRouteError } from 'react-router-dom';
85
89
  import { useSearchParams } from 'react-router-dom';
86
90
  import { useWatch } from 'react-hook-form';
@@ -157,6 +161,12 @@ export declare interface ApiClientConfig {
157
161
  baseURL: string;
158
162
  /** Returns the current bearer token (or null/undefined). Called per request. */
159
163
  getToken?: () => string | null | undefined;
164
+ /**
165
+ * Per-request correlation id sent as the `X-Request-ID` header, matching the
166
+ * Tempest FastAPI SDK `RequestIDMiddleware`. Defaults to a generated id.
167
+ * Return an empty string to disable the header.
168
+ */
169
+ requestId?: () => string;
160
170
  /** Called on 401 responses. Use it to logout the user or trigger a refresh. */
161
171
  onUnauthorized?: (response: Response) => void | Promise<void>;
162
172
  /**
@@ -173,8 +183,26 @@ export declare interface ApiClientConfig {
173
183
  }
174
184
 
175
185
  export declare interface ApiError {
186
+ /** HTTP status code (0 for network failures). */
176
187
  status: number;
188
+ /** Human-readable message — the backend envelope's `detail` (or `message`). */
177
189
  detail: string;
190
+ /**
191
+ * Programmatic error code from the Tempest FastAPI SDK envelope (`code`),
192
+ * e.g. `"EMAIL_TAKEN"`. Lets callers branch without parsing `detail`.
193
+ */
194
+ code?: string;
195
+ /**
196
+ * Correlation id echoed from the backend envelope's `details.request_id`
197
+ * (or the `X-Request-ID` response header). Pair it with `createLogger`.
198
+ */
199
+ requestId?: string;
200
+ /**
201
+ * Seconds to wait before retrying, parsed from the `Retry-After` response
202
+ * header (commonly on `429`/`503`). Honored by {@link retry}.
203
+ */
204
+ retryAfter?: number;
205
+ /** The raw parsed error body, when available. */
178
206
  body?: unknown;
179
207
  }
180
208
 
@@ -558,6 +586,23 @@ export declare const BREAKPOINTS: Record<Breakpoint, number>;
558
586
 
559
587
  export { BrowserRouter }
560
588
 
589
+ /**
590
+ * Parse an error body + response into the Tempest {@link ApiError} envelope.
591
+ *
592
+ * Reads `detail`/`message`, the programmatic `code`, and the correlation id
593
+ * from `details.request_id` (falling back to the `X-Request-ID` header, then
594
+ * the id the client sent).
595
+ *
596
+ * @param status - HTTP status code.
597
+ * @param body - The parsed error body (object, string, or null).
598
+ * @param headers - The response headers (for the `X-Request-ID` fallback).
599
+ * @param sentRequestId - The id the client sent on the request, if any.
600
+ * @returns A fully-populated `ApiError`.
601
+ */
602
+ export declare function buildApiError(status: number, body: unknown, headers?: Headers | {
603
+ get(name: string): string | null;
604
+ }, sentRequestId?: string): ApiError;
605
+
561
606
  /**
562
607
  * Primary action button with variants, sizes and a loading state that
563
608
  * preserves layout via an absolutely-positioned spinner.
@@ -1444,6 +1489,65 @@ export declare interface CreateStorePersistOptions<T> {
1444
1489
  migrate?: PersistOptions<T, Partial<T>>["migrate"];
1445
1490
  }
1446
1491
 
1492
+ /**
1493
+ * Turn-key auth preset wiring `createAuthStore` + `createRefreshQueue` +
1494
+ * `createApiClient` to the Tempest FastAPI SDK auth contract: login returns
1495
+ * `{ access_token, token_type }`, requests carry `Authorization: Bearer`, and a
1496
+ * `401` triggers a single deduplicated refresh + retry. Logout (or a failed
1497
+ * refresh) clears the session.
1498
+ *
1499
+ * @example
1500
+ * const auth = createTempestAuth<User, { email: string; password: string }>({
1501
+ * baseURL: import.meta.env.VITE_API_URL,
1502
+ * mePath: "/api/auth/me",
1503
+ * });
1504
+ *
1505
+ * await auth.login({ email, password }); // stores session, returns the user
1506
+ * const orders = await auth.api.get("/api/orders"); // sends the bearer token
1507
+ * auth.logout();
1508
+ *
1509
+ * @param options - The auth configuration.
1510
+ * @returns The store hook, a wired API client, and login/logout/refresh helpers.
1511
+ */
1512
+ export declare function createTempestAuth<TUser, TCredentials = {
1513
+ email: string;
1514
+ password: string;
1515
+ }>(options: CreateTempestAuthOptions<TUser>): TempestAuth<TUser, TCredentials>;
1516
+
1517
+ export declare interface CreateTempestAuthOptions<TUser> {
1518
+ /** Base URL of the API. Required. */
1519
+ baseURL: string;
1520
+ /** Login route (`POST`). Default: `"/api/auth/login"`. */
1521
+ loginPath?: string;
1522
+ /** Refresh route (`POST`). Default: `"/api/auth/refresh"`. */
1523
+ refreshPath?: string;
1524
+ /** Optional current-user route (`GET`) called after login/refresh. */
1525
+ mePath?: string;
1526
+ /** Persist key for the store. Default: `"tempest-auth"`. */
1527
+ storeName?: string;
1528
+ /** Storage backend. Default: `"local"`. */
1529
+ storage?: "local" | "session";
1530
+ /** Send cookies (needed when the refresh token lives in an httpOnly cookie). */
1531
+ withCredentials?: boolean;
1532
+ /** Custom fetch implementation (testing / SSR). Defaults to `globalThis.fetch`. */
1533
+ fetcher?: typeof fetch;
1534
+ /**
1535
+ * Extract tokens from a login/refresh response. Default reads
1536
+ * `access_token` + `refresh_token`.
1537
+ */
1538
+ parseTokens?: (data: unknown) => {
1539
+ token: string;
1540
+ refreshToken?: string;
1541
+ };
1542
+ /** Pull the user out of the login response, when the API embeds it. */
1543
+ parseUser?: (data: unknown) => TUser | null;
1544
+ /**
1545
+ * Build the refresh request body. Default sends `{ refresh_token }` when a
1546
+ * refresh token is stored, else `undefined` (cookie-based refresh).
1547
+ */
1548
+ refreshBody?: (refreshToken: string | null) => unknown;
1549
+ }
1550
+
1447
1551
  /**
1448
1552
  * Open a WebSocket with automatic exponential-backoff reconnect, optional
1449
1553
  * heartbeat pings, and typed JSON parsing.
@@ -1479,6 +1583,26 @@ export declare interface CreateWebSocketOptions<T> {
1479
1583
  onStatusChange?: (status: WebSocketStatus) => void;
1480
1584
  }
1481
1585
 
1586
+ /** Cursor-paginated response envelope (`CursorPaginationSchema[T]`). */
1587
+ export declare interface CursorPage<T> {
1588
+ /** The rows for this batch. */
1589
+ items: T[];
1590
+ /** Opaque cursor for the next batch, or null when exhausted. Pass it back verbatim. */
1591
+ next_cursor: string | null;
1592
+ /** Whether another batch exists. */
1593
+ has_more: boolean;
1594
+ /** Batch size used for this query. */
1595
+ limit: number;
1596
+ }
1597
+
1598
+ /** Cursor filter query params (`CursorPaginationFilterSchema`). */
1599
+ export declare interface CursorParams {
1600
+ cursor?: string | null;
1601
+ limit?: number;
1602
+ order_by?: string;
1603
+ ascending?: boolean;
1604
+ }
1605
+
1482
1606
  /**
1483
1607
  * Generic, typed list that renders a `<ul>` with one `<li>` per item.
1484
1608
  *
@@ -1778,6 +1902,14 @@ export declare interface ElementSize {
1778
1902
  height: number;
1779
1903
  }
1780
1904
 
1905
+ /**
1906
+ * Build an empty {@link OffsetPage} — handy as `placeholderData`/`initialData`.
1907
+ *
1908
+ * @param pageSize - The page size to report (default 20).
1909
+ * @returns An empty offset page on page 1.
1910
+ */
1911
+ export declare function emptyOffsetPage<T>(pageSize?: number): OffsetPage<T>;
1912
+
1781
1913
  /** Centered "nothing here yet" placeholder with optional icon and CTA. */
1782
1914
  export declare function EmptyState({ icon, title, description, action, className }: EmptyStateProps): JSX.Element;
1783
1915
 
@@ -2579,6 +2711,23 @@ export declare function installSkipWaitingListener(): void;
2579
2711
 
2580
2712
  export declare type InterpolationValues = Record<string, string | number>;
2581
2713
 
2714
+ /**
2715
+ * Type guard for the {@link ApiError} shape. Matches both {@link TempestApiError}
2716
+ * instances and plain objects carrying `status` + `detail`.
2717
+ *
2718
+ * @param error - The unknown value (typically a caught error).
2719
+ * @returns Whether `error` conforms to the `ApiError` contract.
2720
+ */
2721
+ export declare function isApiError(error: unknown): error is ApiError;
2722
+
2723
+ /**
2724
+ * Type guard for a {@link CursorPage} envelope.
2725
+ *
2726
+ * @param value - The unknown value to test.
2727
+ * @returns Whether it carries `items` + `has_more` + a `next_cursor` key.
2728
+ */
2729
+ export declare function isCursorPage<T = unknown>(value: unknown): value is CursorPage<T>;
2730
+
2582
2731
  /**
2583
2732
  * Type guard asserting a value is neither `null` nor `undefined`.
2584
2733
  *
@@ -2628,6 +2777,14 @@ export declare function isJWTExpired(token: string, leewaySeconds?: number): boo
2628
2777
  */
2629
2778
  export declare function isNumber(value: unknown): value is number;
2630
2779
 
2780
+ /**
2781
+ * Type guard for an {@link OffsetPage} envelope.
2782
+ *
2783
+ * @param value - The unknown value to test.
2784
+ * @returns Whether it carries `items` + `total` + `page` + `pages`.
2785
+ */
2786
+ export declare function isOffsetPage<T = unknown>(value: unknown): value is OffsetPage<T>;
2787
+
2631
2788
  /**
2632
2789
  * Type guard asserting a value is a plain object literal.
2633
2790
  *
@@ -3074,6 +3231,33 @@ export declare interface OfflineStoreConfig<TItem> {
3074
3231
  ownerField?: keyof TItem & string;
3075
3232
  }
3076
3233
 
3234
+ /** Offset-paginated response envelope (fastapi-pagination `Page[T]`). */
3235
+ export declare interface OffsetPage<T> {
3236
+ /** The rows for the current page. */
3237
+ items: T[];
3238
+ /** Total number of matching rows across all pages. */
3239
+ total: number;
3240
+ /** Current 1-based page number. */
3241
+ page: number;
3242
+ /** Total number of pages. */
3243
+ pages: number;
3244
+ /** Page size — fastapi-pagination emits `size`. */
3245
+ size?: number;
3246
+ /** Page size — some backends name it `page_size` instead. */
3247
+ page_size?: number;
3248
+ }
3249
+
3250
+ /** Offset filter query params (fastapi-pagination / `BasePaginationFilterSchema`). */
3251
+ export declare interface OffsetParams {
3252
+ page?: number;
3253
+ /** Page size — fastapi-pagination convention. */
3254
+ size?: number;
3255
+ /** Page size — `page_size` convention. */
3256
+ page_size?: number;
3257
+ order_by?: string;
3258
+ ascending?: boolean;
3259
+ }
3260
+
3077
3261
  /**
3078
3262
  * Create a new object with the given `keys` removed from `obj`.
3079
3263
  *
@@ -3672,6 +3856,13 @@ export declare interface RetryOptions {
3672
3856
  initialDelay?: number;
3673
3857
  /** Maximum delay between attempts. Default: 10_000. */
3674
3858
  maxDelay?: number;
3859
+ /**
3860
+ * Honor a `Retry-After` hint on the thrown error (`error.retryAfter`, in
3861
+ * seconds — populated by {@link createApiClient} on `429`/`503`). When
3862
+ * present it overrides the exponential backoff for that attempt. The value
3863
+ * is capped at `maxDelay`. Default: true.
3864
+ */
3865
+ respectRetryAfter?: boolean;
3675
3866
  /**
3676
3867
  * Return false to stop retrying for a specific error.
3677
3868
  * Default: retry on any thrown error.
@@ -4322,6 +4513,46 @@ export declare interface TelemetryUser {
4322
4513
  traits?: Record<string, unknown>;
4323
4514
  }
4324
4515
 
4516
+ /**
4517
+ * Error thrown by {@link createApiClient} / {@link uploadWithProgress} on a
4518
+ * non-2xx response. Mirrors the Tempest FastAPI SDK error envelope
4519
+ * (`{ detail, code, details.request_id }`) so callers get a typed `code` and a
4520
+ * `requestId` for log correlation, while still being a real `Error` (stack
4521
+ * trace, `instanceof Error`).
4522
+ *
4523
+ * @example
4524
+ * try {
4525
+ * await api.post("/users", { body });
4526
+ * } catch (err) {
4527
+ * if (isApiError(err) && err.code === "EMAIL_TAKEN") {
4528
+ * showFieldError("email", err.detail);
4529
+ * }
4530
+ * }
4531
+ */
4532
+ export declare class TempestApiError extends Error implements ApiError {
4533
+ readonly status: number;
4534
+ readonly detail: string;
4535
+ readonly code?: string;
4536
+ readonly requestId?: string;
4537
+ readonly body?: unknown;
4538
+ constructor(init: ApiError);
4539
+ }
4540
+
4541
+ export declare interface TempestAuth<TUser, TCredentials> {
4542
+ /** The persisted Zustand auth store hook (compatible with `<AuthGuard>`). */
4543
+ useAuthStore: ReturnType<typeof createAuthStore<TUser>>;
4544
+ /** A `createApiClient` wired with bearer auth + 401 → refresh → retry. */
4545
+ api: ApiClient;
4546
+ /** Authenticate, store the session, and resolve the user (or null). */
4547
+ login: (credentials: TCredentials) => Promise<TUser | null>;
4548
+ /** Clear the session (and the stored refresh token). */
4549
+ logout: () => void;
4550
+ /** Refresh the access token (deduplicated across concurrent callers). */
4551
+ refresh: () => Promise<void>;
4552
+ /** The current access token, or null. */
4553
+ getToken: () => string | null;
4554
+ }
4555
+
4325
4556
  /**
4326
4557
  * A single declarative route node. Mirrors React Router's nested `<Route>`
4327
4558
  * model but adds first-class `lazy` (code-split with retry) and `guard`
@@ -4354,6 +4585,16 @@ export declare interface TempestRouteObject {
4354
4585
  caseSensitive?: boolean;
4355
4586
  }
4356
4587
 
4588
+ /** The token envelope returned by a Tempest FastAPI SDK login/refresh route. */
4589
+ export declare interface TempestTokenResponse {
4590
+ /** The bearer access token. */
4591
+ access_token: string;
4592
+ /** Token type — always `"bearer"` for the SDK. */
4593
+ token_type?: string;
4594
+ /** Optional refresh token (when the API returns it in the body, not a cookie). */
4595
+ refresh_token?: string;
4596
+ }
4597
+
4357
4598
  /**
4358
4599
  * Multi-line text input. Mirrors the {@link Input} API for label/helper/error.
4359
4600
  */
@@ -4675,7 +4916,7 @@ export declare interface UploadProgressEvent {
4675
4916
  *
4676
4917
  * `fetch` cannot report upload progress in browsers, so this helper falls
4677
4918
  * back to `XMLHttpRequest`. It mirrors the error contract used by
4678
- * {@link createApiClient}: non-2xx responses throw an {@link ApiError}.
4919
+ * {@link createApiClient}: non-2xx responses reject with a `TempestApiError`.
4679
4920
  *
4680
4921
  * @returns The parsed JSON response, or the raw text when the response is not JSON.
4681
4922
  */
@@ -4696,6 +4937,11 @@ export declare interface UploadWithProgressOptions {
4696
4937
  signal?: AbortSignal;
4697
4938
  /** Override the JSON parser. Defaults to `JSON.parse`. */
4698
4939
  parser?: (raw: string) => unknown;
4940
+ /**
4941
+ * Per-request correlation id sent as `X-Request-ID` (Tempest convention).
4942
+ * Defaults to a generated id. Return an empty string to disable.
4943
+ */
4944
+ requestId?: () => string;
4699
4945
  }
4700
4946
 
4701
4947
  /**
@@ -4810,6 +5056,52 @@ export declare interface UseClipboardResult {
4810
5056
  reset: () => void;
4811
5057
  }
4812
5058
 
5059
+ /**
5060
+ * Cursor-pagination hook over TanStack Query's `useInfiniteQuery` for the
5061
+ * Tempest `CursorPaginationSchema` envelope
5062
+ * (`{ items, next_cursor, has_more, limit }`).
5063
+ *
5064
+ * The opaque `next_cursor` is fed straight back as the next page param; the
5065
+ * loop stops when `has_more` is false (or `next_cursor` is null).
5066
+ *
5067
+ * @example
5068
+ * const feed = useCursorQuery<Post>({
5069
+ * queryKey: ["feed"],
5070
+ * limit: 30,
5071
+ * queryFn: (params) => postsService.listPosts(params),
5072
+ * });
5073
+ * // feed.items, feed.fetchNextPage(), feed.hasNextPage
5074
+ */
5075
+ export declare function useCursorQuery<T>(options: UseCursorQueryOptions<T>): UseCursorQueryResult<T>;
5076
+
5077
+ export declare interface UseCursorQueryOptions<T> extends Omit<UseInfiniteQueryOptions<CursorPage<T>, Error, InfiniteData<CursorPage<T>, string | null>, QueryKey, string | null>, "queryKey" | "queryFn" | "initialPageParam" | "getNextPageParam"> {
5078
+ /** Base query key — `limit`/sort is appended for cache isolation. */
5079
+ queryKey: QueryKey;
5080
+ /** Fetcher receiving the cursor params; return the backend envelope. */
5081
+ queryFn: (params: CursorParams) => Promise<CursorPage<T>> | CursorPage<T>;
5082
+ /** Batch size sent as `limit`. Default: 20. */
5083
+ limit?: number;
5084
+ /** Initial `order_by`. */
5085
+ orderBy?: string;
5086
+ /** Initial `ascending`. Default: false. */
5087
+ ascending?: boolean;
5088
+ }
5089
+
5090
+ export declare interface UseCursorQueryResult<T> {
5091
+ /** All rows fetched so far, flattened across batches. */
5092
+ items: T[];
5093
+ /** The raw batches, in fetch order. */
5094
+ pages: CursorPage<T>[];
5095
+ hasNextPage: boolean;
5096
+ isLoading: boolean;
5097
+ isFetchingNextPage: boolean;
5098
+ error: Error | null;
5099
+ /** Fetch the next batch (no-op when exhausted). */
5100
+ fetchNextPage: () => void;
5101
+ /** Refetch from the first batch. */
5102
+ refetch: () => void;
5103
+ }
5104
+
4813
5105
  /**
4814
5106
  * Debounce a fast-changing value. Returns the latest value once `delay` ms
4815
5107
  * have elapsed without further changes.
@@ -5095,6 +5387,71 @@ export declare interface UseOAuthCallbackResult<T> {
5095
5387
  */
5096
5388
  export declare function useOnline(): boolean;
5097
5389
 
5390
+ /**
5391
+ * Offset-pagination hook over TanStack Query for the fastapi-pagination /
5392
+ * Tempest envelope (`{ items, total, page, size, pages }`).
5393
+ *
5394
+ * It owns the page state, sends `page` + the size param (`size` by default,
5395
+ * or `page_size` via `sizeParam`) plus optional `order_by`/`ascending` to your
5396
+ * fetcher, keeps the previous page visible while the next loads, and derives
5397
+ * `hasNext`/`hasPrev`/`pageCount`.
5398
+ *
5399
+ * @example
5400
+ * const users = usePaginatedQuery<User>({
5401
+ * queryKey: ["users"],
5402
+ * pageSize: 25,
5403
+ * queryFn: (params) => usersService.listUsers(params),
5404
+ * });
5405
+ * // users.items, users.next(), users.hasNext, users.pageCount
5406
+ */
5407
+ export declare function usePaginatedQuery<T>(options: UsePaginatedQueryOptions<T>): UsePaginatedQueryResult<T>;
5408
+
5409
+ export declare interface UsePaginatedQueryOptions<T> extends Omit<UseQueryOptions<OffsetPage<T>, Error, OffsetPage<T>, QueryKey>, "queryKey" | "queryFn"> {
5410
+ /** Base query key — the current page/sort is appended for cache isolation. */
5411
+ queryKey: QueryKey;
5412
+ /** Fetcher receiving the offset params; return the backend envelope. */
5413
+ queryFn: (params: OffsetParams) => Promise<OffsetPage<T>> | OffsetPage<T>;
5414
+ /** Initial 1-based page. Default: 1. */
5415
+ initialPage?: number;
5416
+ /** Page size. Default: 20. */
5417
+ pageSize?: number;
5418
+ /**
5419
+ * Query-param name for the page size. fastapi-pagination uses `"size"`
5420
+ * (default); SDK `BasePaginationFilterSchema` configs may use `"page_size"`.
5421
+ */
5422
+ sizeParam?: "size" | "page_size";
5423
+ /** Initial `order_by` (only sent when set). */
5424
+ orderBy?: string;
5425
+ /** Initial `ascending` (only sent when `orderBy` is set). Default: false. */
5426
+ ascending?: boolean;
5427
+ }
5428
+
5429
+ export declare interface UsePaginatedQueryResult<T> {
5430
+ /** The current page envelope, or undefined while the first load is pending. */
5431
+ page: OffsetPage<T> | undefined;
5432
+ /** The rows of the current page (empty array while pending). */
5433
+ items: T[];
5434
+ /** Current 1-based page number (controlled by the hook). */
5435
+ pageNumber: number;
5436
+ /** Total page count from the last successful response. */
5437
+ pageCount: number;
5438
+ /** Total row count from the last successful response. */
5439
+ total: number;
5440
+ hasNext: boolean;
5441
+ hasPrev: boolean;
5442
+ isLoading: boolean;
5443
+ isFetching: boolean;
5444
+ error: Error | null;
5445
+ /** Jump to a specific 1-based page (clamped to >= 1). */
5446
+ setPage: (page: number) => void;
5447
+ /** Go to the next page when available. */
5448
+ next: () => void;
5449
+ /** Go to the previous page when available. */
5450
+ prev: () => void;
5451
+ /** Refetch the current page. */
5452
+ refetch: () => void;
5453
+ }
5454
+
5098
5455
  /**
5099
5456
  * Manage page/size state for paginated lists. Reset goes back to page 1
5100
5457
  * without touching the page size.