/** * Query Parameter Builder Utilities * Provides type-safe query parameter construction and URL building */ import { z } from 'zod'; // Query parameter value types type QueryValue = string | number | boolean | null | undefined; // Query parameter schema for validation export const QueryParamsSchema = z.record( z.union([ z.string(), z.number(), z.boolean(), z.null(), z.undefined(), ]) ).optional(); export type QueryParams = z.infer; /** * Builds URL query string from parameters */ export function buildQueryString(params: QueryParams): string { if (!params || Object.keys(params).length === 0) { return ''; } const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== null && value !== undefined && value !== '') { searchParams.append(key, String(value)); } } const queryString = searchParams.toString(); return queryString ? `?${queryString}` : ''; } /** * Builds a complete URL with query parameters */ export function buildUrl(basePath: string, params?: QueryParams): string { const queryString = buildQueryString(params); return `${basePath}${queryString}`; } /** * Type-safe query parameter builder with fluent interface */ export class QueryBuilder { private queryParams: Record = {}; /** * Add a query parameter */ param(key: string, value: QueryValue): this { if (value !== null && value !== undefined && value !== '') { this.queryParams[key] = value; } return this; } /** * Add multiple parameters at once */ params(params: QueryParams): this { if (params) { for (const [key, value] of Object.entries(params)) { this.param(key, value); } } return this; } /** * Add parameters conditionally */ when(condition: boolean, callback: (builder: this) => this): this { if (condition) { return callback(this); } return this; } /** * Add parameter if value is truthy */ whenTruthy(key: string, value: QueryValue): this { return this.when(!!value, builder => builder.param(key, value)); } /** * Add parameter if value is not null/undefined */ whenDefined(key: string, value: QueryValue): this { return this.when(value != null, builder => builder.param(key, value)); } /** * Build query string */ toString(): string { return buildQueryString(this.queryParams); } /** * Build complete URL */ toUrl(basePath: string): string { return buildUrl(basePath, this.queryParams); } /** * Get raw parameters object */ toParams(): QueryParams { return { ...this.queryParams }; } /** * Clear all parameters */ clear(): this { this.queryParams = {}; return this; } /** * Create a new builder instance */ static create(): QueryBuilder { return new QueryBuilder(); } } /** * Common query parameter patterns */ export const QueryPatterns = { /** * Pagination parameters */ pagination: (page?: number, limit?: number) => QueryBuilder.create() .whenDefined('page', page) .whenDefined('limit', limit), /** * Sorting parameters */ sorting: (sortBy?: string, sortOrder?: 'asc' | 'desc') => QueryBuilder.create() .whenDefined('sort', sortBy) .whenDefined('order', sortOrder), /** * Filtering parameters */ filtering: (filters: Record) => QueryBuilder.create().params(filters), /** * Search parameters */ search: (query?: string, fields?: string[]) => QueryBuilder.create() .whenDefined('q', query) .whenDefined('fields', fields?.join(',')), /** * Date range parameters */ dateRange: (startDate?: string, endDate?: string) => QueryBuilder.create() .whenDefined('start_date', startDate) .whenDefined('end_date', endDate), /** * Common list parameters (search, pagination, sorting, filtering) */ list: (options: { query?: string; page?: number; limit?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; filters?: Record; } = {}) => { const { query, page, limit, sortBy, sortOrder, filters } = options; return QueryBuilder.create() .whenDefined('q', query) .whenDefined('page', page) .whenDefined('limit', limit) .whenDefined('sort', sortBy) .whenDefined('order', sortOrder) .params(filters); }, }; /** * Type-safe query parameter schema builder */ export function createQuerySchema( shape: T, options?: { strict?: boolean } ) { const baseSchema = z.object(shape); if (options?.strict) { return baseSchema.strict(); } return baseSchema; } /** * Validate and build query parameters */ export function validateAndBuildQuery( schema: z.ZodSchema, params: unknown, basePath: string ): string { try { const validated = schema.parse(params); return buildUrl(basePath, validated as QueryParams); } catch (error) { if (import.meta.env.DEV) { console.error('[Query Builder] Query validation failed:', error); } throw new Error('Invalid query parameters'); } }