@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
JavaScript
'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