From d15b90cfcb599d6dbe2ee753cce45c9e886ed23d Mon Sep 17 00:00:00 2001 From: Ilia Mashkov Date: Wed, 7 Jan 2026 16:49:37 +0300 Subject: [PATCH] feat: move buildQueryString to separate directory --- src/shared/lib/utils/buildQueryString.test.ts | 194 ------------------ src/shared/lib/utils/buildQueryString.ts | 79 ------- 2 files changed, 273 deletions(-) delete mode 100644 src/shared/lib/utils/buildQueryString.test.ts delete mode 100644 src/shared/lib/utils/buildQueryString.ts diff --git a/src/shared/lib/utils/buildQueryString.test.ts b/src/shared/lib/utils/buildQueryString.test.ts deleted file mode 100644 index b7e1d7a..0000000 --- a/src/shared/lib/utils/buildQueryString.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -/** - * Tests for buildQueryString utility - */ - -import { - describe, - expect, - test, -} from 'vitest'; -import { buildQueryString } from './buildQueryString'; - -describe('buildQueryString', () => { - describe('basic parameter building', () => { - test('should build query string with string parameter', () => { - const result = buildQueryString({ category: 'serif' }); - expect(result).toBe('?category=serif'); - }); - - test('should build query string with number parameter', () => { - const result = buildQueryString({ limit: 50 }); - expect(result).toBe('?limit=50'); - }); - - test('should build query string with boolean parameter', () => { - const result = buildQueryString({ active: true }); - expect(result).toBe('?active=true'); - }); - - test('should build query string with multiple parameters', () => { - const result = buildQueryString({ - category: 'serif', - limit: 50, - page: 1, - }); - expect(result).toBe('?category=serif&limit=50&page=1'); - }); - }); - - describe('array handling', () => { - test('should handle array of strings', () => { - const result = buildQueryString({ - subsets: ['latin', 'latin-ext', 'cyrillic'], - }); - expect(result).toBe('?subsets=latin&subsets=latin-ext&subsets=cyrillic'); - }); - - test('should handle array of numbers', () => { - const result = buildQueryString({ ids: [1, 2, 3] }); - expect(result).toBe('?ids=1&ids=2&ids=3'); - }); - - test('should handle mixed arrays and primitives', () => { - const result = buildQueryString({ - category: 'serif', - subsets: ['latin', 'latin-ext'], - limit: 50, - }); - expect(result).toBe('?category=serif&subsets=latin&subsets=latin-ext&limit=50'); - }); - - test('should filter out null/undefined values in arrays', () => { - const result = buildQueryString({ - // @ts-expect-error - Testing runtime behavior with invalid types - ids: [1, null, 3, undefined], - }); - expect(result).toBe('?ids=1&ids=3'); - }); - }); - - describe('optional values', () => { - test('should exclude undefined values', () => { - const result = buildQueryString({ - category: 'serif', - search: undefined, - }); - expect(result).toBe('?category=serif'); - }); - - test('should exclude null values', () => { - const result = buildQueryString({ - category: 'serif', - search: null, - }); - expect(result).toBe('?category=serif'); - }); - - test('should handle all undefined/null values', () => { - const result = buildQueryString({ - category: undefined, - search: null, - }); - expect(result).toBe(''); - }); - }); - - describe('URL encoding', () => { - test('should encode spaces', () => { - const result = buildQueryString({ search: 'hello world' }); - expect(result).toBe('?search=hello+world'); - }); - - test('should encode special characters', () => { - const result = buildQueryString({ query: 'a&b=c+d' }); - expect(result).toBe('?query=a%26b%3Dc%2Bd'); - }); - - test('should encode Unicode characters', () => { - const result = buildQueryString({ text: 'café' }); - expect(result).toBe('?text=caf%C3%A9'); - }); - - test('should encode reserved URL characters', () => { - const result = buildQueryString({ url: 'https://example.com' }); - expect(result).toBe('?url=https%3A%2F%2Fexample.com'); - }); - }); - - describe('edge cases', () => { - test('should return empty string for empty object', () => { - const result = buildQueryString({}); - expect(result).toBe(''); - }); - - test('should return empty string when all values are excluded', () => { - const result = buildQueryString({ - a: undefined, - b: null, - }); - expect(result).toBe(''); - }); - - test('should handle empty arrays', () => { - const result = buildQueryString({ tags: [] }); - expect(result).toBe(''); - }); - - test('should handle zero values', () => { - const result = buildQueryString({ page: 0, count: 0 }); - expect(result).toBe('?page=0&count=0'); - }); - - test('should handle false boolean', () => { - const result = buildQueryString({ active: false }); - expect(result).toBe('?active=false'); - }); - - test('should handle empty string', () => { - const result = buildQueryString({ search: '' }); - expect(result).toBe('?search='); - }); - }); - - describe('parameter order', () => { - test('should maintain parameter order from input object', () => { - const result = buildQueryString({ - a: '1', - b: '2', - c: '3', - }); - expect(result).toBe('?a=1&b=2&c=3'); - }); - }); - - describe('real-world examples', () => { - test('should handle Google Fonts API parameters', () => { - const result = buildQueryString({ - category: 'sans-serif', - sort: 'popularity', - subset: 'latin', - }); - expect(result).toBe('?category=sans-serif&sort=popularity&subset=latin'); - }); - - test('should handle Fontshare API parameters', () => { - const result = buildQueryString({ - categories: ['Sans', 'Serif'], - page: 1, - limit: 50, - search: 'satoshi', - }); - expect(result).toBe('?categories=Sans&categories=Serif&page=1&limit=50&search=satoshi'); - }); - - test('should handle pagination parameters', () => { - const result = buildQueryString({ - page: 2, - per_page: 20, - sort: 'name', - order: 'desc', - }); - expect(result).toBe('?page=2&per_page=20&sort=name&order=desc'); - }); - }); -}); diff --git a/src/shared/lib/utils/buildQueryString.ts b/src/shared/lib/utils/buildQueryString.ts deleted file mode 100644 index fc09249..0000000 --- a/src/shared/lib/utils/buildQueryString.ts +++ /dev/null @@ -1,79 +0,0 @@ -/** - * Build query string from URL parameters - * - * Generic, type-safe function to build properly encoded query strings - * from URL parameters. Supports primitives, arrays, and optional values. - * - * @param params - Object containing query parameters - * @returns Encoded query string (empty string if no parameters) - * - * @example - * ```ts - * buildQueryString({ category: 'serif', subsets: ['latin', 'latin-ext'] }) - * // Returns: "category=serif&subsets=latin&subsets=latin-ext" - * - * buildQueryString({ limit: 50, page: 1 }) - * // Returns: "limit=50&page=1" - * - * buildQueryString({}) - * // Returns: "" - * - * buildQueryString({ search: 'hello world', active: true }) - * // Returns: "search=hello%20world&active=true" - * ``` - */ - -/** - * Query parameter value type - * Supports primitives, arrays, and excludes null/undefined - */ -export type QueryParamValue = string | number | boolean | string[] | number[]; - -/** - * Query parameters object - */ -export type QueryParams = Record; - -/** - * Build query string from URL parameters - * - * Handles: - * - Primitive values (string, number, boolean) - * - Arrays (multiple values with same key) - * - Optional values (excludes undefined/null) - * - Proper URL encoding - * - * Edge cases: - * - Empty object → empty string - * - No parameters → empty string - * - Nested objects → flattens to string representation - * - Special characters → proper encoding - * - * @param params - Object containing query parameters - * @returns Encoded query string (with "?" prefix if non-empty) - */ -export function buildQueryString(params: QueryParams): string { - const searchParams = new URLSearchParams(); - - for (const [key, value] of Object.entries(params)) { - // Skip undefined/null values - if (value === undefined || value === null) { - continue; - } - - // Handle arrays (multiple values with same key) - if (Array.isArray(value)) { - for (const item of value) { - if (item !== undefined && item !== null) { - searchParams.append(key, String(item)); - } - } - } else { - // Handle primitives - searchParams.append(key, String(value)); - } - } - - const queryString = searchParams.toString(); - return queryString ? `?${queryString}` : ''; -}