@letsparky/api-v2-client
Version:
TypeScript client for the LetsParky API V2
240 lines (239 loc) • 9.34 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ApiClient = void 0;
const fetch_1 = require("./adapters/fetch");
const types_1 = require("./types");
const logger_1 = require("./logger");
const errors_1 = require("./errors");
/**
* Extract JWT expiration timestamp
*/
function extractJwtExpiration(token) {
try {
const parts = token.split('.');
if (parts.length !== 3)
return null;
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
return payload.exp ? payload.exp * 1000 : null;
}
catch {
return null;
}
}
/**
* Simplified API client for LetsParky API v2
*/
class ApiClient {
constructor(config = {}) {
this.rateLimitInfo = null;
const { apiKey, baseUrl, timeout = 30000, email, password, environment = types_1.Environment.PRODUCTION, logLevel = types_1.LogLevel.INFO, silentLogs = false } = config;
this.apiKey = apiKey;
this.email = email;
this.password = password;
this.environment = environment;
this.timeout = timeout;
this.logger = new logger_1.Logger({
level: logLevel,
silent: silentLogs
});
this.baseUrl = baseUrl || this.getDefaultBaseUrl();
// Auto-authenticate if email/password provided
if (this.email && this.password) {
this.authenticate().catch(err => {
this.logger.error('Auto-authentication failed:', err);
});
}
}
getDefaultBaseUrl() {
const hosts = {
[types_1.Environment.DEVELOPMENT]: 'http://localhost:3000',
[types_1.Environment.STAGING]: 'https://test.api-v2.letsparky.com',
[types_1.Environment.PRODUCTION]: 'https://api-v2.letsparky.com'
};
return `${hosts[this.environment]}/api/v2`;
}
buildUrl(path, params) {
// Ensure path starts with / and remove leading / from path to append properly
const cleanPath = path.startsWith('/') ? path.substring(1) : path;
// Ensure baseUrl ends with / for proper joining
const baseUrlWithSlash = this.baseUrl.endsWith('/') ? this.baseUrl : `${this.baseUrl}/`;
const url = new URL(cleanPath, baseUrlWithSlash);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value != null) {
url.searchParams.set(key, String(value));
}
});
}
return url.toString();
}
createHeaders(customHeaders) {
const headers = new Headers({
'Accept': 'application/json',
'Content-Type': 'application/json',
...customHeaders
});
// Add authentication
if (this.authToken && this.authToken.token && this.authToken.expiresAt > Date.now()) {
headers.set('Authorization', `Bearer ${this.authToken.token}`);
}
else if (this.apiKey) {
headers.set('Authorization', `Bearer ${this.apiKey}`);
}
return headers;
}
async makeRequest(method, path, data, params) {
const url = this.buildUrl(path, params);
const headers = this.createHeaders();
const context = {
method: method.toUpperCase(),
url
};
try {
console.log("Making request to", url, "with token", this.authToken);
// Check if token needs refresh
if (this.authToken && this.authToken.expiresAt - Date.now() < 30000) {
try {
await this.refreshToken();
}
catch (err) {
this.logger.warn('Token refresh failed, removing auth:', err);
this.setToken('', 0); // Clear token
}
}
this.logger.debug('Making request:', { url, method, params });
const response = await (0, fetch_1.fetchWithTimeout)(url, {
method,
headers,
body: data ? JSON.stringify(data) : undefined,
timeout: this.timeout
});
// Update rate limit info
this.rateLimitInfo = (0, fetch_1.extractRateLimitInfo)(response.headers);
const parsedResponse = await (0, fetch_1.parseResponse)(response);
// Handle error status codes
if (response.status >= 400) {
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('retry-after') || '60');
throw new errors_1.RateLimitError(`Rate limit exceeded. Retry after ${retryAfter} seconds.`, retryAfter, context);
}
if (response.status === 401 && this.email && this.password) {
await this.authenticate();
return this.makeRequest(method, path, data, params);
}
const message = this.extractErrorMessage(parsedResponse.data);
throw new errors_1.ApiError(message, 'API_ERROR', response.status, parsedResponse.data, context);
}
// Auto-capture tokens from auth responses
if (path.includes('/auth/') && parsedResponse.data && typeof parsedResponse.data === 'object') {
this.handleAuthResponse(parsedResponse.data);
}
return parsedResponse;
}
catch (error) {
if (error instanceof errors_1.ApiError || error instanceof errors_1.RateLimitError) {
throw error;
}
throw (0, errors_1.createStandardError)(error, 'Request failed', context);
}
}
extractErrorMessage(data) {
var _a;
if (typeof data === 'string')
return data;
if (data === null || data === void 0 ? void 0 : data.message)
return data.message;
if ((_a = data === null || data === void 0 ? void 0 : data.error) === null || _a === void 0 ? void 0 : _a.message)
return data.error.message;
return 'API request failed';
}
handleAuthResponse(data) {
var _a, _b;
const token = data.token || ((_a = data.data) === null || _a === void 0 ? void 0 : _a.token);
const expiresIn = data.expiresIn || ((_b = data.data) === null || _b === void 0 ? void 0 : _b.expiresIn);
if (token) {
this.setToken(token, expiresIn);
}
}
async refreshToken() {
var _a;
if (!this.email || !this.password) {
throw new errors_1.AuthenticationError('Credentials required for token refresh', {});
}
try {
const response = await this.post('/auth/refresh', {});
if ((_a = response.data) === null || _a === void 0 ? void 0 : _a.token) {
this.setToken(response.data.token, response.data.expiresIn);
}
}
catch {
// If refresh fails, do full auth
await this.authenticate();
}
}
// Public methods
setApiKey(apiKey) {
this.apiKey = apiKey;
}
setToken(token, expiresIn) {
if (!token) {
this.authToken = undefined;
return;
}
console.log("Setting token", token);
const expiresAt = expiresIn
? Date.now() + (expiresIn * 1000)
: extractJwtExpiration(token) || Date.now() + 3600000;
this.authToken = { token, expiresAt };
}
getToken() {
var _a;
return ((_a = this.authToken) === null || _a === void 0 ? void 0 : _a.token) || null;
}
isAuthenticated() {
return !!(this.apiKey || (this.authToken && this.authToken.token && this.authToken.expiresAt > Date.now()));
}
setEnvironment(environment) {
this.environment = environment;
this.baseUrl = this.baseUrl || this.getDefaultBaseUrl();
}
setLogLevel(level) {
this.logger.setLevel(level);
}
async authenticate() {
var _a;
if (!this.email || !this.password) {
throw new errors_1.AuthenticationError('Email and password required', {});
}
const response = await this.post('/auth/login', {
email: this.email,
password: this.password
});
if (!((_a = response.data) === null || _a === void 0 ? void 0 : _a.token)) {
throw new errors_1.AuthenticationError('No token received', {});
}
this.setToken(response.data.token, response.data.expiresIn);
}
getRateLimitInfo() {
return this.rateLimitInfo;
}
// HTTP methods
async get(path, params) {
return this.makeRequest('GET', path, undefined, params);
}
async getPaginated(path, paginationParams, queryParams) {
const params = { ...queryParams, ...paginationParams };
const response = await this.makeRequest('GET', path, undefined, params);
return response.data;
}
async post(path, data) {
return this.makeRequest('POST', path, data);
}
async put(path, data) {
return this.makeRequest('PUT', path, data);
}
async delete(path, data) {
return this.makeRequest('DELETE', path, data);
}
}
exports.ApiClient = ApiClient;