UNPKG

@follow-app/client-sdk

Version:

TypeScript client SDK for Follow RSS Server API

263 lines (225 loc) 7.38 kB
import type { ModuleDefinition } from "../shared/define-module" import { RouteResolver } from "../shared/route-resolver" import type { HTTPMethod, RequestContentType, RequestOptions, ResponseContentType, } from "../types" import type { HttpClient } from "./base" /** * Route function arguments for legacy compatibility */ export interface LegacyRouteArgs { params?: Record<string, string> query?: Record<string, string | number | boolean> body?: unknown headers?: Record<string, string> timeout?: number signal?: AbortSignal } /** * Route function arguments with flattened input */ export interface RouteArgs extends Record<string, any> { headers?: Record<string, string> timeout?: number signal?: AbortSignal } /** * Route function type */ export type RouteFunction<T = any> = (args?: RouteArgs) => Promise<T> /** * Route definition for API endpoints (from module system) */ export interface RouteDefinition { 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[] requestType?: RequestContentType responseType?: ResponseContentType } /** * Dynamic proxy handler for API routes */ export class APIProxyHandler { private routes: Map<string, RouteDefinition> private client: HttpClient constructor(client: HttpClient, routes: Map<string, RouteDefinition>) { this.client = client this.routes = routes } /** * Proxy get trap that creates route functions dynamically */ get(target: any, propKey: string | symbol): any { const routeKey = String(propKey) // Handle special properties if (routeKey === "constructor" || routeKey === "prototype") { return target[propKey] } // Handle nested routes (e.g., feeds.claim.challenge) if (routeKey.includes(".")) { return this.handleNestedRoute(routeKey) } const route = this.routes.get(routeKey) if (!route) { // For nested routes, return a proxy that continues the chain if (this.hasNestedRoute(routeKey)) { return this.createNestedProxy(routeKey) } throw new Error(`Route '${routeKey}' not found`) } return this.createRouteFunction(route) } /** * Check if a route key has nested routes */ private hasNestedRoute(prefix: string): boolean { for (const routeKey of this.routes.keys()) { if (routeKey.startsWith(`${prefix}.`)) { return true } } return false } /** * Create a nested proxy for route chains */ private createNestedProxy(prefix: string): any { const nestedRoutes = new Map<string, RouteDefinition>() // Find all routes that start with the prefix for (const [routeKey, route] of this.routes.entries()) { if (routeKey.startsWith(`${prefix}.`)) { const nestedKey = routeKey.slice(Math.max(0, prefix.length + 1)) nestedRoutes.set(nestedKey, route) } } return new Proxy({}, new APIProxyHandler(this.client, nestedRoutes)) } /** * Handle nested route calls */ private handleNestedRoute(routeKey: string): RouteFunction { const route = this.routes.get(routeKey) if (!route) { throw new Error(`Nested route '${routeKey}' not found`) } return this.createRouteFunction(route) } /** * Create a route function from a route definition */ private createRouteFunction(route: RouteDefinition): RouteFunction { return async (args: RouteArgs = {}) => { const { headers, timeout, signal, ...inputArgs } = args // Check if this is using the old API structure (legacy compatibility) const params: Record<string, string> = {} const query: Record<string, string | number | boolean> = {} let body: unknown // Handle new flattened API structure const flattenedArgs = { ...inputArgs } // Extract parameters from flattenedArgs based on route definition if (route.params) { route.params.forEach((param) => { if (flattenedArgs[param] !== undefined) { params[param] = String(flattenedArgs[param]) // Remove param from flattenedArgs to avoid duplication delete flattenedArgs[param] } }) } // Extract query parameters based on route definition if (route.query) { route.query.forEach((field) => { if (flattenedArgs[field] !== undefined) { query[field] = flattenedArgs[field] // Remove from flattenedArgs to avoid duplication delete flattenedArgs[field] } }) } // Extract body parameters based on route definition if (route.body) { const bodyData: Record<string, any> = {} route.body.forEach((field) => { if (flattenedArgs[field] !== undefined) { bodyData[field] = flattenedArgs[field] // Remove from flattenedArgs to avoid duplication delete flattenedArgs[field] } }) if (Object.keys(bodyData).length > 0) { body = bodyData } } // Handle remaining fields with fallback logic (backward compatibility) // For GET requests, remaining args go to query // For POST/PUT/PATCH/DELETE requests, remaining args go to body // Filter out undefined values const remainingFields = Object.keys(flattenedArgs).filter( (key) => flattenedArgs[key] !== undefined, ) if (remainingFields.length > 0) { const remainingData = Object.fromEntries( remainingFields.map((key) => [key, flattenedArgs[key]]), ) if (route.method === "GET") { Object.assign(query, remainingData) } else { // For non-GET requests, merge remaining args into body if (body && typeof body === "object") { Object.assign(body, remainingData) } else { body = remainingData } } } // Build URL with parameters let url = route.path if (route.params) { route.params.forEach((param) => { const value = params[param] if (value !== undefined) { url = url.replace(`{${param}}`, encodeURIComponent(value)) } }) } // Build request options const requestOptions: RequestOptions = { method: route.method, headers, timeout, signal, requestType: route.requestType, } // Add query parameters if they exist if (Object.keys(query).length > 0) { requestOptions.query = query } // Add body if it exists if (body !== undefined) { requestOptions.body = body } return this.client.request(url, requestOptions) } } } /** * Create a typed API proxy from a module definition */ export function createAPIProxy<T extends Record<string, any>>( client: HttpClient, module: ModuleDefinition<any>, ): T { // Flatten nested routes and apply prefix const flatRoutes = RouteResolver.flattenRoutes(module.routes, module.prefix) const routeMap = new Map(Object.entries(flatRoutes)) return new Proxy({}, new APIProxyHandler(client, routeMap)) as T } // Legacy functions removed - new module system only