neuwo-api
Version:
TypeScript/JavaScript SDK client for the Neuwo content classification API
357 lines • 13.4 kB
JavaScript
;
/**
* Utility functions and HTTP request handler for Neuwo API.
*
* Provides low-level HTTP communication, parameter encoding,
* validation, and response parsing.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.RequestHandler = void 0;
exports.validateUrl = validateUrl;
exports.parseJsonResponse = parseJsonResponse;
exports.sanitiseContent = sanitiseContent;
exports.sleep = sleep;
exports.formatDate = formatDate;
const errors_js_1 = require("./errors.js");
const logger_js_1 = require("./logger.js");
/**
* Validate URL format.
* Throws ValidationError if URL is invalid.
*
* @param url - URL string to validate
* @throws {ValidationError} If URL is invalid or empty
*/
function validateUrl(url) {
if (!url || typeof url !== "string") {
throw new errors_js_1.ValidationError("URL must be a non-empty string");
}
try {
new URL(url);
}
catch {
throw new errors_js_1.ValidationError(`Invalid URL format: ${url}`);
}
}
/**
* Parse JSON response from API.
* Throws ContentNotAvailableError if response contains an error field.
*
* @param response - Fetch API Response object
* @returns Parsed JSON data (caller should validate/cast to expected type)
* @throws {ContentNotAvailableError} If response contains an error field
* @throws {Error} If response is not valid JSON
*/
async function parseJsonResponse(response) {
const text = await response.text();
try {
const data = JSON.parse(text);
// Check for error field (EDGE API specific)
if (data.error) {
throw new errors_js_1.ContentNotAvailableError(data.error, data.url);
}
return data;
}
catch (error) {
if (error instanceof errors_js_1.ContentNotAvailableError) {
throw error;
}
logger_js_1.logger.error(`Failed to parse JSON response: ${text.substring(0, 200)}`);
throw new Error(`Invalid JSON response: ${text.substring(0, 100)}`);
}
}
/**
* Validate and sanitise content string.
* Throws ValidationError if content is empty or only whitespace.
*
* @param content - Content string to validate
* @returns Original content if valid
* @throws {ValidationError} If content is empty or only whitespace
*/
function sanitiseContent(content) {
if (!content || typeof content !== "string") {
throw new errors_js_1.ValidationError("Content must be a non-empty string");
}
const trimmed = content.trim();
if (trimmed.length === 0) {
throw new errors_js_1.ValidationError("Content cannot be empty or only whitespace");
}
return content;
}
/**
* Sleep for specified milliseconds.
*
* @param ms - Number of milliseconds to sleep
* @returns Promise that resolves after the specified time
*/
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/**
* Format a Date to YYYY-MM-DD string.
*
* @param date - Date object to format
* @returns Formatted date string in YYYY-MM-DD format
*/
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
/**
* Request handler for making HTTP requests to Neuwo API.
*/
class RequestHandler {
/**
* @param token - API authentication token
* @param baseUrl - Base URL for API requests
* @param timeout - Request timeout in seconds
*/
constructor(token, baseUrl, timeout) {
this.token = token;
this.baseUrl = baseUrl;
this.timeout = timeout;
}
/**
* Build full URL with query parameters including token.
*
* @param endpoint - API endpoint path
* @param params - Optional query parameters to append
* @returns Complete URL string with token and parameters
*/
buildUrl(endpoint, params) {
// Ensure baseUrl ends with / and endpoint doesn't start with / for clean joining
const base = this.baseUrl.endsWith("/")
? this.baseUrl
: this.baseUrl + "/";
const path = endpoint.startsWith("/") ? endpoint.slice(1) : endpoint;
const url = new URL(path, base);
// Always add token as query parameter
url.searchParams.append("token", this.token);
// Add additional parameters
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
// For arrays, repeat the parameter name
for (const item of value) {
url.searchParams.append(key, String(item));
}
}
else {
url.searchParams.append(key, String(value));
}
}
}
}
return url.toString();
}
/**
* Encode a value for form data.
*
* @param value - Value to encode (string, number, boolean, Date, etc.)
* @returns String representation of the value
*/
encodeValue(value) {
if (typeof value === "boolean") {
return value ? "true" : "false";
}
return String(value);
}
/**
* Encode data as application/x-www-form-urlencoded.
*
* Handles arrays by repeating the parameter name for each value.
* For example: {tags: ['a', 'b']} becomes 'tags=a&tags=b'
*
* @param data - Data object to encode
* @returns URL-encoded string
*/
encodeFormData(data) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
if (value !== null && value !== undefined) {
if (Array.isArray(value)) {
// Repeat parameter for each value in array
for (const item of value) {
if (item !== null && item !== undefined) {
params.append(key, this.encodeValue(item));
}
}
}
else {
params.append(key, this.encodeValue(value));
}
}
}
return params.toString();
}
/**
* Handle API error responses by parsing and creating appropriate exceptions.
*
* Parses the error response, extracts relevant error information, and maps
* HTTP status codes to specific exception types for better error handling.
*
* @param response - HTTP response object with status code >= 400
* @returns Appropriate exception instance based on the status code and error content
*/
static async handleAPIError(response) {
const statusCode = response.status;
const responseText = await response.text();
let errorData = {};
let message = null;
let detail;
let validationErrors;
// Try to parse response as JSON
try {
errorData = JSON.parse(responseText);
}
catch {
errorData = {};
logger_js_1.logger.debug("Could not parse error response as JSON");
// Store raw response text as detail for non-JSON responses
if (responseText && responseText.length < 500) {
detail = responseText;
}
}
logger_js_1.logger.error(`API error ${statusCode}: ${errorData && Object.keys(errorData).length > 0
? JSON.stringify(errorData)
: responseText.slice(0, 500)}`);
// Extract message
if (errorData.message && typeof errorData.message === "string") {
message = errorData.message;
}
else if (errorData.detail && typeof errorData.detail === "string") {
message = errorData.detail;
}
else if (errorData.error && typeof errorData.error === "string") {
message = errorData.error;
}
// Store detail separately if it's not used as message
if (errorData.detail && errorData.detail !== message) {
detail = errorData.detail;
if (Array.isArray(detail)) {
validationErrors = detail;
}
}
// Fallback to response text if no message found
if (!message) {
if (responseText && responseText.length < 500) {
message = responseText;
}
else {
message = `API error with status ${statusCode}`;
}
}
// Merge detail into message if it contains additional information
if (detail && typeof detail === "string" && detail !== message) {
message = `${message}: ${detail}`;
}
// Map status codes to specific errors
switch (statusCode) {
case 400:
return new errors_js_1.BadRequestError(message);
case 401:
return new errors_js_1.AuthenticationError(message);
case 403:
return new errors_js_1.ForbiddenError(message);
case 404:
// Check if this is "No data yet available" error
if (message.toLowerCase().includes("no data yet available")) {
return new errors_js_1.NoDataAvailableError(message);
}
return new errors_js_1.NotFoundError(message);
case 422:
// Extract validation details if present
if (Array.isArray(validationErrors)) {
return new errors_js_1.ValidationError(message, validationErrors);
}
return new errors_js_1.ValidationError(message);
case 429: {
// Extract retry_after from headers if present
let retryAfter;
const retryAfterHeader = response.headers.get("Retry-After");
if (retryAfterHeader) {
try {
retryAfter = parseInt(retryAfterHeader, 10);
}
catch {
logger_js_1.logger.debug(`Could not parse Retry-After header: ${retryAfterHeader}`);
}
}
return new errors_js_1.RateLimitError(message, retryAfter);
}
case 500:
case 502:
case 503:
case 504:
return new errors_js_1.ServerError(message, statusCode);
default:
if (statusCode >= 500) {
return new errors_js_1.ServerError(message, statusCode);
}
return new errors_js_1.NeuwoAPIError(message, statusCode);
}
}
/**
* Make an HTTP request to the API.
*
* @param options - Request configuration options
* @returns Fetch API Response object
* @throws {NetworkError} On network failure or timeout
* @throws {NeuwoAPIError} On API error responses
*/
async request(options) {
const { method, endpoint, params, data, headers } = options;
// Build full URL with query parameters and token
const url = this.buildUrl(endpoint, params);
// Prepare headers
const requestHeaders = {
...headers,
};
// Prepare request body
let body;
if (data && method !== "GET") {
// Form URL encoded data
requestHeaders["Content-Type"] =
"application/x-www-form-urlencoded";
body = this.encodeFormData(data);
}
logger_js_1.logger.debug(`Making ${method} request to ${url}`);
try {
// Create abort controller for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.timeout * 1000);
const response = await fetch(url, {
method,
headers: requestHeaders,
body,
signal: controller.signal,
});
clearTimeout(timeoutId);
logger_js_1.logger.debug(`Response status: ${response.status}`);
// Handle error status codes
if (!response.ok) {
throw await RequestHandler.handleAPIError(response);
}
return response;
}
catch (error) {
if (error instanceof Error) {
if (error.name === "AbortError") {
logger_js_1.logger.error(`Request timeout after ${this.timeout} seconds`);
throw new errors_js_1.NetworkError(`Request timeout after ${this.timeout} seconds`, error);
}
if (error.name === "TypeError" &&
error.message.includes("fetch")) {
logger_js_1.logger.error(`Connection error: ${error.message}`);
throw new errors_js_1.NetworkError("Failed to connect to API server", error);
}
}
// Re-throw if it's already one of our custom errors
throw error;
}
}
}
exports.RequestHandler = RequestHandler;
//# sourceMappingURL=utils.js.map