UNPKG

@swimmable/sdk

Version:

Official JavaScript/TypeScript SDK for the Swimmable API - Real-time swimming conditions and water quality data

507 lines (500 loc) 16.3 kB
'use strict'; var fetch = require('cross-fetch'); /** * Swimmable API Client * Official JavaScript/TypeScript SDK for the Swimmable API */ class SwimmableClient { constructor(config = {}) { this.config = { apiKey: config.apiKey || '', baseUrl: config.baseUrl || 'https://api.swimmable.app', timeout: config.timeout || 10000, headers: config.headers || {}, }; } /** * Make an HTTP request to the Swimmable API */ async request(endpoint, options = {}) { const { method = 'GET', body, timeout = this.config.timeout, headers = {}, requiresAuth = false, } = options; const url = `${this.config.baseUrl}${endpoint}`; const requestHeaders = { 'Content-Type': 'application/json', 'User-Agent': 'Swimmable-JS-SDK/1.0.0', ...this.config.headers, ...headers, }; // Add authentication header if API key is provided or required if (this.config.apiKey) { requestHeaders['X-API-Key'] = this.config.apiKey; } else if (requiresAuth) { throw new Error('API key is required for this endpoint. Please provide an apiKey in the configuration.'); } const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, { method, headers: requestHeaders, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, }); clearTimeout(timeoutId); const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); let data; const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { data = await response.json(); } else { data = (await response.text()); } if (!response.ok) { const error = data; throw new Error(error.message || `HTTP ${response.status}: ${response.statusText}`); } return { data, status: response.status, headers: responseHeaders, }; } catch (error) { clearTimeout(timeoutId); if (error instanceof Error) { if (error.name === 'AbortError') { throw new Error(`Request timeout after ${timeout}ms`); } throw error; } throw new Error('Unknown error occurred'); } } /** * Get basic swimming conditions for a location * @param coordinates - Latitude and longitude * @param options - Request options */ async getConditions(coordinates, options) { const endpoint = `/api/public/conditions?lat=${coordinates.lat}&lon=${coordinates.lon}`; const response = await this.request(endpoint, options); return response.data; } /** * Get enhanced swimming conditions with detailed analysis * @param coordinates - Latitude and longitude * @param options - Request options */ async getEnhancedConditions(coordinates, options) { const endpoint = `/api/public/conditions/enhanced?lat=${coordinates.lat}&lon=${coordinates.lon}`; const response = await this.request(endpoint, options); return response.data; } /** * Get list of available swimming spots * @param options - Request options */ async getSpots(options) { const endpoint = '/api/public/spots'; const response = await this.request(endpoint, options); return response.data; } /** * Get API health status * @param options - Request options */ async getHealth(options) { const endpoint = '/api/health'; const response = await this.request(endpoint, options); return response.data; } /** * Get your API usage statistics (requires API key) * @param days - Number of days to look back (default: 30) * @param options - Request options */ async getUsageStats(days = 30, options) { const endpoint = `/api/keys/usage?days=${days}`; const response = await this.request(endpoint, { ...options, requiresAuth: true, }); return response.data; } /** * Get your API keys (requires authentication) * @param options - Request options */ async getApiKeys(options) { const endpoint = '/api/keys'; const response = await this.request(endpoint, { ...options, requiresAuth: true, }); return response.data.api_keys; } /** * Create a new API key (requires authentication) * @param keyData - API key creation data * @param options - Request options */ async createApiKey(keyData, options) { const endpoint = '/api/keys'; const response = await this.request(endpoint, { ...options, method: 'POST', body: keyData, requiresAuth: true, }); return response.data; } /** * Revoke an API key (requires authentication) * @param keyId - ID of the API key to revoke * @param options - Request options */ async revokeApiKey(keyId, options) { const endpoint = `/api/keys/${keyId}`; const response = await this.request(endpoint, { ...options, method: 'DELETE', requiresAuth: true, }); return response.data; } /** * Update the API key for this client * @param apiKey - New API key */ setApiKey(apiKey) { this.config.apiKey = apiKey; } /** * Get the current configuration */ getConfig() { return { ...this.config }; } /** * Update client configuration * @param config - Configuration updates */ updateConfig(config) { this.config = { ...this.config, ...config }; } } /** * Create a new Swimmable client instance * @param config - Client configuration */ function createClient(config) { return new SwimmableClient(config); } /** * Default client instance (can be configured globally) */ new SwimmableClient(); /** * Utility classes and functions for the Swimmable SDK */ /** * Custom error class for Swimmable API errors */ /** * Utility functions for working with coordinates and locations */ class LocationUtils { /** * Validate that coordinates are within valid ranges * @param coordinates - Coordinates to validate */ static validateCoordinates(coordinates) { const { lat, lon } = coordinates; return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180; } /** * Calculate the distance between two coordinates using the Haversine formula * @param coord1 - First coordinate * @param coord2 - Second coordinate * @returns Distance in kilometers */ static calculateDistance(coord1, coord2) { const R = 6371; // Earth's radius in kilometers const dLat = this.toRadians(coord2.lat - coord1.lat); const dLon = this.toRadians(coord2.lon - coord1.lon); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(coord1.lat)) * Math.cos(this.toRadians(coord2.lat)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } /** * Convert degrees to radians * @param degrees - Degrees to convert */ static toRadians(degrees) { return degrees * (Math.PI / 180); } /** * Find the nearest spot from a list of coordinates * @param target - Target coordinates * @param spots - Array of spots with coordinates */ static findNearestSpot(target, spots) { if (spots.length === 0) return null; let nearest = spots[0]; let minDistance = this.calculateDistance(target, nearest); for (let i = 1; i < spots.length; i++) { const distance = this.calculateDistance(target, spots[i]); if (distance < minDistance) { minDistance = distance; nearest = spots[i]; } } return nearest; } } /** * Utility functions for working with swimming conditions */ class ConditionsUtils { /** * Convert temperature from Celsius to Fahrenheit * @param celsius - Temperature in Celsius */ static celsiusToFahrenheit(celsius) { return (celsius * 9 / 5) + 32; } /** * Convert temperature from Fahrenheit to Celsius * @param fahrenheit - Temperature in Fahrenheit */ static fahrenheitToCelsius(fahrenheit) { return (fahrenheit - 32) * 5 / 9; } /** * Get a human-readable description of the swimmability score * @param score - Swimmability score (1-10) */ static getSwimmabilityDescription(score) { if (score >= 9) return 'Excellent swimming conditions'; if (score >= 7) return 'Good swimming conditions'; if (score >= 5) return 'Fair swimming conditions'; if (score >= 3) return 'Poor swimming conditions'; return 'Dangerous swimming conditions'; } /** * Get a color code for the swimmability score (useful for UI) * @param score - Swimmability score (1-10) */ static getSwimmabilityColor(score) { if (score >= 8) return '#10B981'; // Green if (score >= 6) return '#F59E0B'; // Yellow if (score >= 4) return '#F97316'; // Orange return '#EF4444'; // Red } /** * Check if conditions are safe for swimming based on various factors * @param conditions - Enhanced conditions object */ static isSafeForSwimming(conditions) { // Basic safety checks if (conditions.swimmabilityScore < 5) return false; if (conditions.warnings && conditions.warnings.length > 0) return false; // Check water temperature (too cold can be dangerous) const waterTemp = conditions.conditions?.water?.temperature?.value; if (waterTemp && waterTemp < 15) return false; // Below 15°C is generally too cold return true; } } /** * Utility functions for API key management */ class ApiKeyUtils { /** * Validate API key format * @param apiKey - API key to validate */ static validateApiKey(apiKey) { // Swimmable API keys start with 'swm_' followed by 64 hex characters return /^swm_[a-f0-9]{64}$/.test(apiKey); } /** * Get the prefix of an API key (for display purposes) * @param apiKey - Full API key */ static getKeyPrefix(apiKey) { if (apiKey.length < 12) return apiKey; return `${apiKey.substring(0, 12)}...`; } /** * Mask an API key for secure display * @param apiKey - API key to mask */ static maskApiKey(apiKey) { if (apiKey.length < 8) return '***'; return `${apiKey.substring(0, 4)}${'*'.repeat(apiKey.length - 8)}${apiKey.substring(apiKey.length - 4)}`; } } /** * Rate limiting utilities for client-side throttling */ class RateLimiter { constructor(maxRequests = 100, windowMs = 60000) { this.requests = []; this.maxRequests = maxRequests; this.windowMs = windowMs; } /** * Check if a request can be made without exceeding rate limits */ canMakeRequest() { const now = Date.now(); // Remove requests outside the current window this.requests = this.requests.filter(time => now - time < this.windowMs); return this.requests.length < this.maxRequests; } /** * Record a request (call this after making a successful request) */ recordRequest() { this.requests.push(Date.now()); } /** * Get the number of requests remaining in the current window */ getRemainingRequests() { const now = Date.now(); this.requests = this.requests.filter(time => now - time < this.windowMs); return Math.max(0, this.maxRequests - this.requests.length); } /** * Get the time until the rate limit window resets (in milliseconds) */ getResetTime() { if (this.requests.length === 0) return 0; const oldestRequest = Math.min(...this.requests); const resetTime = oldestRequest + this.windowMs; return Math.max(0, resetTime - Date.now()); } } /** * Exception classes for the Swimmable JavaScript SDK */ /** * Base exception class for all Swimmable SDK errors */ class SwimmableError extends Error { constructor(message, status, code, endpoint) { super(message); this.name = 'SwimmableError'; this.status = status; this.code = code; this.endpoint = endpoint; } } /** * Exception raised for API-related errors (4xx, 5xx responses) */ class SwimmableAPIError extends SwimmableError { constructor(message, status, code, endpoint, responseData) { super(message, status, code, endpoint); this.name = 'SwimmableAPIError'; this.responseData = responseData; } } /** * Exception raised when a request times out */ class SwimmableTimeoutError extends SwimmableError { constructor(message = 'Request timed out', timeout) { super(message); this.name = 'SwimmableTimeoutError'; this.timeout = timeout; } } /** * Exception raised for client-side validation errors */ class SwimmableValidationError extends SwimmableError { constructor(message) { super(message); this.name = 'SwimmableValidationError'; } } /** * Exception raised for authentication-related errors (401, 403) */ class SwimmableAuthenticationError extends SwimmableAPIError { constructor(message, status, code, endpoint) { super(message, status, code, endpoint); this.name = 'SwimmableAuthenticationError'; } } /** * Exception raised when rate limits are exceeded (429) */ class SwimmableRateLimitError extends SwimmableAPIError { constructor(message, retryAfter, endpoint) { super(message, 429, 'RATE_LIMIT_EXCEEDED', endpoint); this.name = 'SwimmableRateLimitError'; this.retryAfter = retryAfter; } } /** * Swimmable JavaScript/TypeScript SDK * Official SDK for the Swimmable API - Real-time swimming conditions and water quality data * * @example * ```typescript * import { SwimmableClient } from '@swimmable/sdk'; * * const client = new SwimmableClient({ * apiKey: 'your-api-key-here' * }); * * // Get basic conditions * const conditions = await client.getConditions({ lat: 34.0522, lon: -118.2437 }); * console.log(`Water temperature: ${conditions.waterTemperature}°C`); * * // Get enhanced conditions with safety scores * const enhanced = await client.getEnhancedConditions({ lat: 34.0522, lon: -118.2437 }); * console.log(`Swimmability score: ${enhanced.swimmabilityScore}/10`); * ``` */ // Export the main client class and factory function // Version information const VERSION = '1.0.0'; exports.ApiKeyUtils = ApiKeyUtils; exports.ConditionsUtils = ConditionsUtils; exports.LocationUtils = LocationUtils; exports.RateLimiter = RateLimiter; exports.SwimmableAPIError = SwimmableAPIError; exports.SwimmableAuthenticationError = SwimmableAuthenticationError; exports.SwimmableClient = SwimmableClient; exports.SwimmableError = SwimmableError; exports.SwimmableRateLimitError = SwimmableRateLimitError; exports.SwimmableTimeoutError = SwimmableTimeoutError; exports.SwimmableValidationError = SwimmableValidationError; exports.VERSION = VERSION; exports.createClient = createClient; //# sourceMappingURL=index.js.map