talisik-shortener
Version:
JavaScript/TypeScript client for Talisik URL Shortener - A privacy-focused URL shortening service
521 lines (514 loc) • 15.5 kB
JavaScript
'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