@follow-app/client-sdk
Version:
TypeScript client SDK for Follow RSS Server API
164 lines (148 loc) • 3.89 kB
text/typescript
/* eslint-disable @typescript-eslint/no-empty-object-type */
import type {
HTTPMethod,
RequestContentType,
ResponseContentType,
} from "../types"
/**
* Route definition with type annotations
*/
export interface RouteDefinition<TInput = any, TResponse = any> {
method: HTTPMethod
path: string
params?: readonly string[]
/** Fields that should be sent as query parameters */
query?: readonly string[]
/** Fields that should be sent as request body */
body?: readonly string[]
input?: TInput
response?: TResponse
requestType?: RequestContentType
responseType?: ResponseContentType
}
/**
* Nested routes structure - supports any depth of nesting
*/
export type NestedRoutes = {
[key: string]: RouteDefinition | NestedRoutes
}
/**
* Module definition interface
*/
export interface ModuleDefinition<TRoutes extends NestedRoutes> {
name: string
prefix?: string
routes: TRoutes
api: ModuleAPI<TRoutes>
}
/**
* Fetch options for route requests
*/
interface FetchOptions {
headers?: Record<string, string>
timeout?: number
signal?: AbortSignal
}
/**
* Check if arguments are required
*/
type IsRequired<TInput> = [TInput] extends [never] ?
false :
{} extends TInput ?
false :
true
/**
* Route function with proper argument requirements
*/
export type RouteFunction<TInput, TResponse> =
IsRequired<TInput> extends true ?
(args: TInput, options?: FetchOptions) => Promise<TResponse> :
(args?: TInput, options?: FetchOptions) => Promise<TResponse>
/**
* Generate API types from route structure
*/
export type ModuleAPI<TRoutes> = {
[K in keyof TRoutes]: TRoutes[K] extends RouteDefinition<
infer TInput,
infer TResponse
> ?
RouteFunction<TInput, TResponse> :
TRoutes[K] extends NestedRoutes ?
ModuleAPI<TRoutes[K]> :
never;
}
/**
* Legacy route args interface for backward compatibility
*/
export interface LegacyRouteArgs {
params?: Record<string, string>
query?: Record<string, string | number | boolean>
body?: unknown
headers?: Record<string, string>
timeout?: number
signal?: AbortSignal
}
/**
* Helper function to define a single route with proper type inference
*/
export function defineRoute<TInput = never, TResponse = never>(
method: HTTPMethod,
path: string,
options: {
/** Fields that should be sent as query parameters */
query?: readonly string[]
/** Fields that should be sent as request body */
body?: readonly string[]
input?: TInput
response?: TResponse
requestType?: RequestContentType
responseType?: ResponseContentType
} = {},
): RouteDefinition<TInput, TResponse> {
// Extract parameters from path string
const params = extractParamsFromPath(path)
return {
method,
path,
params: params.length > 0 ? params : undefined,
query: options.query,
body: options.body,
input: options.input,
response: options.response,
requestType: options.requestType,
responseType: options.responseType,
}
}
/**
* Extract parameter names from a path string
* e.g. "/users/{userId}/posts" -> ["userId"]
*/
function extractParamsFromPath(path: string): string[] {
const paramRegex = /\{([^}]+)\}/g
const params: string[] = []
let match
while ((match = paramRegex.exec(path)) !== null) {
params.push(match[1])
}
return params
}
/**
* Helper type to infer params from path string
*/
export type InferParams<T extends string> =
T extends `${string}{${infer Param}}${infer Rest}` ?
{ [K in Param]: string } & InferParams<Rest> :
{}
/**
* Define a module with routes and metadata
*/
export function defineModule<TRoutes extends NestedRoutes>(definition: {
name: string
prefix?: string
routes: TRoutes
}): ModuleDefinition<TRoutes> {
return {
...definition,
api: {} as ModuleAPI<TRoutes>, // Will be created by proxy
}
}