UNPKG

talisik-shortener

Version:

JavaScript/TypeScript client for Talisik URL Shortener - A privacy-focused URL shortening service

521 lines (514 loc) 15.5 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * Error classes for Talisik URL Shortener client */ /** * Custom error class for Talisik-specific errors */ class TalisikError extends Error { constructor(message, status, code, details) { super(message); this.name = "TalisikError"; this.status = status; this.code = code; this.details = details; // Maintains proper stack trace for where our error was thrown (only available on V8) if (Error.captureStackTrace) { Error.captureStackTrace(this, TalisikError); } } /** * Check if this is a network-related error */ isNetworkError() { return !this.status || this.status === 408; } /** * Check if this is a client error (4xx) */ isClientError() { return !!this.status && this.status >= 400 && this.status < 500; } /** * Check if this is a server error (5xx) */ isServerError() { return !!this.status && this.status >= 500 && this.status < 600; } /** * Check if this error indicates the resource was not found */ isNotFound() { return this.status === 404; } /** * Check if this error indicates a timeout */ isTimeout() { return (this.status === 408 || this.message.toLowerCase().includes("timeout")); } /** * Convert error to JSON-serializable object */ toJSON() { return { name: this.name, message: this.message, status: this.status, code: this.code, details: this.details, }; } } /** * Error thrown when the Talisik client is not properly configured */ class TalisikConfigError extends TalisikError { constructor(message) { super(message); this.name = "TalisikConfigError"; } } /** * Error thrown when a URL validation fails */ class TalisikValidationError extends TalisikError { constructor(message) { super(message); this.name = "TalisikValidationError"; } } /** * Main client class for interacting with Talisik URL Shortener API * * @example * ```typescript * const client = new TalisikClient({ * baseUrl: 'https://api.talisik.com' * }); * * // Shorten a URL * const result = await client.shorten({ * url: 'https://example.com', * customCode: 'my-link' * }); * * // Get URL info * const info = await client.getUrlInfo('my-link'); * ``` */ class TalisikClient { constructor(config) { this.config = { timeout: 10000, headers: {}, apiKey: "", ...config, }; // Add default headers this.config.headers = { "Content-Type": "application/json", ...this.config.headers, }; // Add API key header if provided if (this.config.apiKey) { this.config.headers["Authorization"] = `Bearer ${this.config.apiKey}`; } } /** * Shorten a URL * * @param request - The URL shortening request * @param options - Additional request options * @returns Promise that resolves to the shortened URL information * * @example * ```typescript * const result = await client.shorten({ * url: 'https://example.com', * customCode: 'my-custom-code', * expiresHours: 24 * }); * ``` */ async shorten(request, options) { const response = await this.request("POST", "/shorten", { url: request.url, custom_code: request.customCode, expires_hours: request.expiresHours, }, options); return { shortUrl: response.short_url, originalUrl: response.original_url, shortCode: response.short_code, expiresAt: response.expires_at, }; } /** * Get information about a shortened URL * * @param shortCode - The short code to look up * @param options - Additional request options * @returns Promise that resolves to URL info or null if not found * * @example * ```typescript * const info = await client.getUrlInfo('abc123'); * if (info) { * console.log(`URL has been clicked ${info.clickCount} times`); * } * ``` */ async getUrlInfo(shortCode, options) { try { const response = await this.request("GET", `/info/${shortCode}`, undefined, options); return { shortCode: response.short_code, originalUrl: response.original_url, createdAt: response.created_at, expiresAt: response.expires_at, clickCount: response.click_count, isActive: response.is_active, isExpired: response.is_expired, }; } catch (error) { if (error instanceof TalisikError && error.status === 404) { return null; } throw error; } } /** * Get overall statistics * * @param options - Additional request options * @returns Promise that resolves to usage statistics * * @example * ```typescript * const stats = await client.getStats(); * console.log(`Total URLs: ${stats.totalUrls}`); * ``` */ async getStats(options) { const response = await this.request("GET", "/api/stats", undefined, options); return { totalUrls: response.total_urls, activeUrls: response.active_urls, totalClicks: response.total_clicks, }; } /** * Get all shortened URLs for table display * * @param options - Additional request options * @returns Promise that resolves to array of URL records * * @example * ```typescript * const urls = await client.getAllUrls(); * console.log(`Found ${urls.length} URLs`); * ``` */ async getAllUrls(options) { const response = await this.request("GET", "/api/urls", undefined, options); return response.urls || []; } /** * Get the redirect URL for a short code (without following the redirect) * * @param shortCode - The short code * @returns The full redirect URL * * @example * ```typescript * const redirectUrl = client.getRedirectUrl('abc123'); * // Returns: https://api.talisik.com/abc123 * ``` */ getRedirectUrl(shortCode) { return `${this.config.baseUrl}/${shortCode}`; } /** * Expand a short code to get the original URL (follows redirect) * * @param shortCode - The short code to expand * @param options - Additional request options * @returns Promise that resolves to the original URL or null if not found * * @example * ```typescript * const originalUrl = await client.expand('abc123'); * // Returns: https://example.com * ``` */ async expand(shortCode, options) { try { // First try HEAD request to get redirect without following it const response = await fetch(`${this.config.baseUrl}/${shortCode}`, { method: "HEAD", headers: this.config.headers, signal: options?.signal, redirect: "manual", }); if (response.status === 301 || response.status === 302) { return response.headers.get("location"); } // If HEAD is not supported (405) or doesn't return redirect, fall back to info endpoint if (response.status === 405 || response.status === 404) { const info = await this.getUrlInfo(shortCode, options); return info ? info.originalUrl : null; } return null; } catch (error) { // If HEAD request fails, try using the info endpoint as fallback try { const info = await this.getUrlInfo(shortCode, options); return info ? info.originalUrl : null; } catch (infoError) { throw new TalisikError(`Failed to expand URL: ${error}`); } } } /** * Low-level request method */ async request(method, endpoint, body, options) { const url = `${this.config.baseUrl}${endpoint}`; const timeout = options?.timeout || this.config.timeout; const headers = { ...this.config.headers, ...options?.headers, }; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); // Use the provided signal or our timeout signal const signal = options?.signal || controller.signal; try { const response = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined, signal, }); clearTimeout(timeoutId); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new TalisikError(errorData.detail || `HTTP ${response.status}: ${response.statusText}`, response.status, errorData.code, errorData); } return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof TalisikError) { throw error; } if (error.name === "AbortError") { throw new TalisikError("Request timeout", 408); } throw new TalisikError(`Network error: ${error.message}`); } } } /** * Factory function to create a new Talisik client instance * * @param config - Client configuration * @returns A new TalisikClient instance * * @example * ```typescript * import { createTalisikClient } from 'talisik-shortener'; * * const client = createTalisikClient({ * baseUrl: 'https://api.talisik.com' * }); * ``` */ function createTalisikClient(config) { return new TalisikClient(config); } /** * React hooks for Talisik URL Shortener * * These hooks are optional and only available if React is installed. * They provide an easy way to integrate Talisik with React applications. * * @example * ```typescript * import { useTalisik } from 'talisik-shortener'; * * function MyComponent() { * const { shortenUrl, loading, error } = useTalisik({ * baseUrl: 'https://api.talisik.com' * }); * * const handleShorten = async () => { * const result = await shortenUrl({ url: 'https://example.com' }); * console.log(result.shortUrl); * }; * * return ( * <button onClick={handleShorten} disabled={loading}> * {loading ? 'Shortening...' : 'Shorten URL'} * </button> * ); * } * ``` */ // Check if React is available at runtime function getReact() { try { // Try to access React through global/window if available if (typeof window !== "undefined" && window.React) { return window.React; } // Try dynamic import for Node.js environments if (typeof globalThis !== "undefined" && globalThis.require) { return globalThis.require("react"); } return null; } catch { return null; } } /** * React hook for using Talisik URL Shortener * * @param options - Configuration options * @returns Object with shortening functions, loading state, and errors */ function useTalisik(options) { const React = getReact(); if (!React) { console.warn("React is not available. useTalisik hook cannot be used."); return undefined; } const { useState, useCallback, useMemo } = React; const [loading, setLoading] = useState(false); const errorState = useState(null); const error = errorState[0]; const setError = errorState[1]; // Create client instance const client = useMemo(() => { return new TalisikClient({ baseUrl: options.baseUrl || "http://localhost:8000", apiKey: options.apiKey, headers: options.headers, timeout: options.timeout, }); }, [options.baseUrl, options.apiKey, options.headers, options.timeout]); const shortenUrl = useCallback(async (request) => { setLoading(true); setError(null); try { const result = await client.shorten(request); return result; } catch (err) { const talisikError = err instanceof TalisikError ? err : new TalisikError(`Unknown error: ${err}`); setError(talisikError); throw talisikError; } finally { setLoading(false); } }, [client]); const getUrlInfo = useCallback(async (shortCode) => { setLoading(true); setError(null); try { const result = await client.getUrlInfo(shortCode); return result; } catch (err) { const talisikError = err instanceof TalisikError ? err : new TalisikError(`Unknown error: ${err}`); setError(talisikError); throw talisikError; } finally { setLoading(false); } }, [client]); const getStats = useCallback(async () => { setLoading(true); setError(null); try { const result = await client.getStats(); return result; } catch (err) { const talisikError = err instanceof TalisikError ? err : new TalisikError(`Unknown error: ${err}`); setError(talisikError); throw talisikError; } finally { setLoading(false); } }, [client]); return { shortenUrl, getUrlInfo, getStats, loading, error, }; } /** * React hook for creating a Talisik client instance * * @param config - Client configuration * @returns Memoized TalisikClient instance * * @example * ```typescript * function MyComponent() { * const client = useTalisikClient({ * baseUrl: 'https://api.talisik.com' * }); * * const handleShorten = async () => { * const result = await client.shorten({ url: 'https://example.com' }); * }; * * return <button onClick={handleShorten}>Shorten</button>; * } * ``` */ function useTalisikClient(config) { const React = getReact(); if (!React) { console.warn("React is not available. useTalisikClient hook cannot be used."); return undefined; } const { useMemo } = React; return useMemo(() => { return new TalisikClient(config); }, [config.baseUrl, config.apiKey, config.headers, config.timeout]); } // Export hook utilities const hooks = { useTalisik, useTalisikClient, }; exports.TalisikClient = TalisikClient; exports.TalisikConfigError = TalisikConfigError; exports.TalisikError = TalisikError; exports.TalisikValidationError = TalisikValidationError; exports.createTalisikClient = createTalisikClient; exports.default = TalisikClient; exports.hooks = hooks; exports.useTalisik = useTalisik; exports.useTalisikClient = useTalisikClient; //# sourceMappingURL=index.js.map