@tantainnovative/ndpr-toolkit
Version:
Nigeria Data Protection Toolkit — enterprise-grade compliance components for the Nigeria Data Protection Act (NDPA) 2023
255 lines (241 loc) • 9.57 kB
text/typescript
/**
* Production-ready API storage adapter.
*
* Backward-compatible with the 3.5.x signature — `apiAdapter('/api/x')`
* still works exactly as before. New options are all opt-in.
*
* @example basic
* const adapter = apiAdapter<ConsentSettings>('/api/consent');
*
* @example with credentials and CSRF
* const adapter = apiAdapter<ConsentSettings>('/api/consent', {
* credentials: 'include',
* headers: () => ({
* 'X-CSRF-Token': document.querySelector<HTMLMetaElement>(
* 'meta[name="csrf-token"]'
* )?.content ?? '',
* }),
* });
*
* @example with retry + telemetry
* const adapter = apiAdapter<ConsentSettings>('/api/consent', {
* retry: { attempts: 2, baseDelayMs: 300 },
* onError: (ctx) => Sentry.captureException(ctx.error, { extra: ctx }),
* onSuccess: (ctx) => analytics.track('consent_saved', { method: ctx.method }),
* });
*
* @example with response unwrap
* const adapter = apiAdapter<ConsentSettings>('/api/consent', {
* // API returns { data: ConsentSettings, ok: true }
* unwrap: (raw) => (raw as { data: ConsentSettings }).data,
* });
*/
export declare function apiAdapter<T = unknown>(endpoint: string, options?: ApiAdapterOptions<T>): StorageAdapter<T>;
export declare interface ApiAdapterErrorContext<T = unknown> {
/** Which adapter operation triggered this — `load`, `save`, or `remove`. */
method: ApiAdapterMethod;
/** The endpoint URL that failed. */
endpoint: string;
/** Underlying error (for network failures / parse errors). */
error?: unknown;
/** Response object, if a response was received. */
response?: Response;
/** HTTP status code, if available. */
status?: number;
/** For `save`, the payload that failed to send. */
payload?: T;
/** Which retry attempt this is (0 = first try). Capped at `retry.attempts`. */
attempt: number;
}
export declare type ApiAdapterMethod = 'load' | 'save' | 'remove';
export declare interface ApiAdapterOptions<T = unknown> {
/**
* Extra HTTP headers to send with every request. Useful for `Authorization`,
* `X-CSRF-Token`, `X-Requested-With`, etc.
*
* Can also be a function that returns headers, which lets you read a CSRF
* token from the DOM/cookie at request time rather than at adapter
* construction time.
*/
headers?: Record<string, string> | (() => Record<string, string>);
/**
* Forwarded to fetch's `credentials` option. Defaults to `'same-origin'`
* (the browser default). Set to `'include'` for cross-origin endpoints
* that need cookies / auth.
*/
credentials?: RequestCredentials;
/**
* HTTP method override for the load operation. Defaults to `'GET'`.
*/
loadMethod?: 'GET' | 'POST';
/**
* HTTP method override for the save operation. Defaults to `'POST'`. Some
* REST APIs prefer `'PUT'` for upsert semantics.
*/
saveMethod?: 'POST' | 'PUT' | 'PATCH';
/**
* Transform the raw JSON response into the expected `T`. Useful for APIs
* that wrap responses in `{ data: ... }` or similar envelopes. Called
* after `res.json()`. If omitted, the parsed JSON is used as-is.
*/
unwrap?: (raw: unknown) => T | null;
/**
* Retry policy for failed requests. Defaults to no retries (preserves the
* pre-3.6.0 behaviour). When configured, applies to all three operations.
*/
retry?: ApiAdapterRetryConfig;
/**
* Called when a request fails (after all retries exhausted). The adapter
* still returns a graceful null/void result so the consuming hook
* doesn't crash — this hook is for telemetry, toasts, or audit logging.
*/
onError?: (ctx: ApiAdapterErrorContext<T>) => void;
/**
* Called when a request succeeds. Useful for cache invalidation,
* analytics, or syncing other state.
*/
onSuccess?: (ctx: ApiAdapterSuccessContext<T>) => void;
/**
* Per-request fetch options to merge into every request. Use this for
* things `fetch` itself supports that aren't directly modelled above —
* `signal`, `mode`, `cache`, `redirect`, etc.
*/
fetchInit?: Omit<RequestInit, 'method' | 'headers' | 'body' | 'credentials'>;
}
export declare interface ApiAdapterRetryConfig {
/**
* Number of additional attempts after the initial request. Defaults to 0
* (no retries). e.g. `attempts: 2` means up to 3 total requests.
*/
attempts?: number;
/**
* Base delay in ms between attempts. Defaults to 250ms. The actual delay
* uses exponential backoff: `baseDelayMs * 2^attempt`.
*/
baseDelayMs?: number;
/**
* Predicate that decides whether to retry given the failure context. By
* default we retry on network errors and 5xx responses, but not on 4xx
* (those are client errors that won't fix themselves).
*/
shouldRetry?: (ctx: ApiAdapterErrorContext<unknown>) => boolean;
}
export declare interface ApiAdapterSuccessContext<T = unknown> {
/** Which adapter operation succeeded — `load`, `save`, or `remove`. */
method: ApiAdapterMethod;
/** The endpoint URL. */
endpoint: string;
/** Response object. */
response: Response;
/** For `load` operations, the parsed (and optionally unwrapped) data. */
data?: T;
/** For `save` operations, the payload that was sent. */
payload?: T;
}
/**
* Compose a primary adapter with one or more secondary adapters. Reads
* always go to the primary; writes (`save` / `remove`) fan out to the
* primary first, then each secondary. Secondary failures are logged but
* never propagated.
*
* Useful for shadowing localStorage to an API endpoint, mirroring consent
* to a cookie for SSR, or building offline-first sync.
*
* @example
* ```ts
* import {
* composeAdapters,
* localStorageAdapter,
* apiAdapter,
* } from '@tantainnovative/ndpr-toolkit/adapters';
* import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks';
*
* const adapter = composeAdapters(
* localStorageAdapter('ndpr_consent'),
* apiAdapter('/api/consent'),
* );
* useConsent({ options, adapter });
* ```
*/
export declare function composeAdapters<T = unknown>(primary: StorageAdapter<T>, ...secondaries: StorageAdapter<T>[]): StorageAdapter<T>;
/**
* Storage adapter backed by a browser cookie. Useful for consent state that
* needs to be shared with server-side rendering, or for cross-subdomain
* persistence via the `domain` option.
*
* @example
* ```ts
* import { cookieAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
* import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks';
*
* const adapter = cookieAdapter('ndpr_consent', {
* domain: '.example.com',
* sameSite: 'Lax',
* expires: 180,
* });
* useConsent({ options, adapter });
* ```
*/
export declare function cookieAdapter<T = unknown>(key: string, options?: CookieAdapterOptions): StorageAdapter<T>;
export declare interface CookieAdapterOptions {
domain?: string;
path?: string;
expires?: number;
secure?: boolean;
sameSite?: 'Strict' | 'Lax' | 'None';
}
/**
* Storage adapter backed by `window.localStorage`. The default adapter used
* by every hook in the toolkit when no `adapter` prop is supplied.
*
* Safe to import server-side — every method short-circuits when
* `window` is undefined, so calling `load()` on the server returns `null`.
*
* @example
* ```ts
* import { localStorageAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
* import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks';
*
* const adapter = localStorageAdapter('ndpr_consent');
* useConsent({ options, adapter });
* ```
*/
export declare function localStorageAdapter<T = unknown>(key: string): StorageAdapter<T>;
/**
* Storage adapter backed by an in-memory value. Useful in tests, Storybook,
* SSR previews, or anywhere persistence across reloads is undesirable.
*
* @example
* ```ts
* import { memoryAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
* import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks';
*
* const adapter = memoryAdapter({ consents: {}, version: '1.0' });
* useConsent({ options, adapter });
* ```
*/
export declare function memoryAdapter<T = unknown>(initialData?: T): StorageAdapter<T>;
/**
* Storage adapter backed by `window.sessionStorage`. Data is scoped to the
* current tab and discarded when the tab closes — useful for consent
* choices that should not survive a fresh session.
*
* @example
* ```ts
* import { sessionStorageAdapter } from '@tantainnovative/ndpr-toolkit/adapters';
* import { useConsent } from '@tantainnovative/ndpr-toolkit/hooks';
*
* const adapter = sessionStorageAdapter('ndpr_consent');
* useConsent({ options, adapter });
* ```
*/
export declare function sessionStorageAdapter<T = unknown>(key: string): StorageAdapter<T>;
export declare interface StorageAdapter<T = unknown> {
/** Load persisted data. Called once on hook mount. */
load(): T | null | Promise<T | null>;
/** Persist data. Called on every state change. */
save(data: T): void | Promise<void>;
/** Clear persisted data. Called on reset. */
remove(): void | Promise<void>;
}
export { }