UNPKG

@neatsuite/http

Version:

A TypeScript-first NetSuite API client with built-in OAuth 1.0a authentication, retry logic, and performance monitoring

578 lines (573 loc) 15.4 kB
import axios from 'axios'; import OAuth from 'oauth-1.0a'; import crypto from 'crypto'; import http from 'http'; import https from 'https'; // src/client.ts // src/types.ts var NetSuiteError = class _NetSuiteError extends Error { constructor(message, status, code, details, response) { super(message); this.name = "NetSuiteError"; this.status = status; this.code = code; this.details = details; this.response = response; Error.captureStackTrace(this, _NetSuiteError); } }; // src/client.ts var pRetry; import('p-retry').then((module) => { pRetry = module; }); var NetSuiteClient = class { constructor(config, logger) { this.middlewares = []; this.config = { timeout: 15e3, retries: 3, enablePerformanceLogging: false, ...config }; this.logger = logger; this.oauth = new OAuth({ consumer: { key: config.oauth.consumerKey, secret: config.oauth.consumerSecret }, signature_method: "HMAC-SHA256", hash_function: (base_string, key) => { return crypto.createHmac("sha256", key).update(base_string).digest("base64"); }, realm: config.oauth.realm }); this.axiosInstance = axios.create({ timeout: this.config.timeout, headers: { "Content-Type": "application/json", "Accept-Encoding": "gzip, deflate", ...this.config.headers }, httpAgent: new http.Agent({ keepAlive: true }), httpsAgent: new https.Agent({ keepAlive: true }), maxRedirects: 5, validateStatus: (status) => status < 500 }); if (this.config.enablePerformanceLogging || logger) { this.setupInterceptors(); } } /** * Add middleware to the request pipeline * @param middleware - Middleware function */ use(middleware) { this.middlewares.push(middleware); } /** * Setup axios interceptors for logging */ setupInterceptors() { this.axiosInstance.interceptors.request.use( (config) => { if (this.logger) { this.logger.debug("NetSuite API Request", { method: config.method, url: config.url, headers: config.headers }); } return config; }, (error) => { if (this.logger) { this.logger.error("NetSuite API Request Error", { error }); } return Promise.reject(error); } ); this.axiosInstance.interceptors.response.use( (response) => { if (this.logger) { this.logger.debug("NetSuite API Response", { status: response.status, url: response.config.url, duration: response.config?.metadata?.duration }); } return response; }, (error) => { if (this.logger) { this.logger.error("NetSuite API Response Error", { status: error.response?.status, url: error.config?.url, error: error.message }); } return Promise.reject(error); } ); } /** * Generate OAuth authorization header */ generateAuthHeader(url, method) { const request_data = { url, method }; const token = { key: this.config.oauth.tokenKey, secret: this.config.oauth.tokenSecret }; return this.oauth.toHeader(this.oauth.authorize(request_data, token)); } /** * Measure performance of an operation */ async measurePerformance(operation, fn) { const startTime = Date.now(); try { const result = await fn(); const duration = Date.now() - startTime; if (this.config.enablePerformanceLogging) { this.logger?.info(`[Performance] ${operation} completed`, { duration }); } return { result, duration }; } catch (error) { const duration = Date.now() - startTime; this.logger?.error(`[Performance] ${operation} failed`, { duration, error }); throw error; } } /** * Execute middleware chain */ async executeMiddlewares(context, index, finalHandler) { if (index >= this.middlewares.length) { return finalHandler(); } const middleware = this.middlewares[index]; return middleware( context, () => this.executeMiddlewares(context, index + 1, finalHandler) ); } /** * Make a request to NetSuite API * * @example * ```typescript * const response = await client.request({ * url: 'https://account.restlets.api.netsuite.com/app/site/hosting/restlet.nl', * method: 'POST', * body: { action: 'search', query: 'customer' } * }); * ``` */ async request(options) { const { url, method = "GET", body = null, headers = {}, retries = this.config.retries, axiosConfig = {} } = options; const authHeader = this.generateAuthHeader(url, method); const requestConfig = { method, url, headers: { ...authHeader, ...headers }, ...axiosConfig, metadata: {} }; if (body && ["POST", "PUT", "PATCH"].includes(method)) { requestConfig.data = body; } const retryOptions = { retries: retries || 3, minTimeout: 1e3, maxTimeout: 3e3, shouldRetry: (error) => { if (error.response?.status >= 400 && error.response?.status < 500) { return false; } return true; }, onFailedAttempt: (error) => { this.logger?.warn(`NetSuite API retry attempt`, { attemptNumber: error.attemptNumber, retriesLeft: error.retriesLeft }); } }; const makeRequest = async () => { const startTime = Date.now(); requestConfig.metadata.startTime = startTime; const response2 = await this.executeMiddlewares( { config: options, authHeaders: authHeader, startTime }, 0, () => this.axiosInstance(requestConfig) ); requestConfig.metadata.duration = Date.now() - startTime; if (response2.status !== 200) { throw new NetSuiteError( `NetSuite API returned status ${response2.status}`, response2.status, "HTTP_ERROR", response2.data, response2 ); } return response2; }; const { result: response, duration } = await this.measurePerformance( `NetSuite API ${method} ${url}`, () => pRetry(makeRequest, retryOptions) ); return { data: response.data, status: response.status, headers: response.headers, duration }; } /** * Build NetSuite RESTlet URL */ buildRestletUrl(params) { const baseUrl = `https://${this.config.accountId}.restlets.api.netsuite.com/app/site/hosting/restlet.nl`; const queryParams = new URLSearchParams({ script: params.script.toString(), deploy: params.deploy.toString(), ...params.params }); return `${baseUrl}?${queryParams.toString()}`; } /** * Make a RESTlet call * * @example * ```typescript * const response = await client.restlet({ * script: '123', * deploy: '1', * params: { action: 'getCustomer', id: '456' } * }); * ``` */ async restlet(params, options) { const url = this.buildRestletUrl(params); return this.request({ url, ...options }); } /** * GET request helper */ async get(url, options) { return this.request({ ...options, url, method: "GET" }); } /** * POST request helper */ async post(url, body, options) { return this.request({ ...options, url, method: "POST", body }); } /** * PUT request helper */ async put(url, body, options) { return this.request({ ...options, url, method: "PUT", body }); } /** * PATCH request helper */ async patch(url, body, options) { return this.request({ ...options, url, method: "PATCH", body }); } /** * DELETE request helper */ async delete(url, options) { return this.request({ ...options, url, method: "DELETE" }); } /** * Handle API errors */ static isNetSuiteError(error) { return error instanceof NetSuiteError; } /** * Create error from axios error */ static createError(error) { if (error.code === "ECONNABORTED" || error.code === "ETIMEDOUT") { return new NetSuiteError( "Request timeout - NetSuite API is taking too long to respond", 504, "TIMEOUT" ); } if (error.response) { return new NetSuiteError( error.response.data?.detail || "Error from NetSuite API", error.response.status, error.response.data?.["o:errorCode"], error.response.data, error.response ); } return new NetSuiteError( error.message || "Unknown error occurred", 500, "UNKNOWN_ERROR" ); } }; // src/utils.ts var ResponseCache = class { constructor() { this.cache = /* @__PURE__ */ new Map(); } /** * Get cached response */ get(key) { const cached = this.cache.get(key); if (!cached) return null; if (Date.now() > cached.expiry) { this.cache.delete(key); return null; } return cached.data; } /** * Set cached response */ set(key, data, ttl) { this.cache.set(key, { data, expiry: Date.now() + ttl * 1e3 }); } /** * Clear cache */ clear() { this.cache.clear(); } /** * Remove specific key */ delete(key) { return this.cache.delete(key); } }; var RateLimiter = class { constructor(maxRequests = 100, windowMs = 6e4) { this.requests = []; this.maxRequests = maxRequests; this.windowMs = windowMs; } /** * Check if request can be made */ canMakeRequest() { const now = Date.now(); this.requests = this.requests.filter((time) => now - time < this.windowMs); return this.requests.length < this.maxRequests; } /** * Record a request */ recordRequest() { this.requests.push(Date.now()); } /** * Get remaining requests */ 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 time until next request can be made */ getTimeUntilNextRequest() { if (this.canMakeRequest()) return 0; const oldestRequest = Math.min(...this.requests); return Math.max(0, this.windowMs - (Date.now() - oldestRequest)); } }; var RequestBatcher = class { constructor(batchProcessor, batchSize = 10, batchDelay = 50) { this.batch = []; this.batchProcessor = batchProcessor; this.batchSize = batchSize; this.batchDelay = batchDelay; } /** * Add request to batch */ add(key) { return new Promise((resolve, reject) => { this.batch.push({ key, resolve, reject }); if (this.batch.length >= this.batchSize) { this.processBatch(); } else if (!this.batchTimeout) { this.batchTimeout = setTimeout(() => this.processBatch(), this.batchDelay); } }); } /** * Process the current batch */ async processBatch() { if (this.batchTimeout) { clearTimeout(this.batchTimeout); this.batchTimeout = void 0; } const currentBatch = this.batch.splice(0, this.batchSize); if (currentBatch.length === 0) return; const keys = currentBatch.map((item) => item.key); try { const results = await this.batchProcessor(keys); currentBatch.forEach(({ key, resolve, reject }) => { const result = results.get(key); if (result !== void 0) { resolve(result); } else { reject(new Error(`No result for key: ${key}`)); } }); } catch (error) { currentBatch.forEach(({ reject }) => reject(error)); } } }; async function retryWithBackoff(fn, options = {}) { const { maxRetries = 3, initialDelay = 1e3, maxDelay = 3e4, factor = 2, onRetry } = options; let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error; if (attempt === maxRetries) { throw error; } if (onRetry) { onRetry(attempt + 1, error); } const delay = Math.min( initialDelay * Math.pow(factor, attempt), maxDelay ); await new Promise((resolve) => setTimeout(resolve, delay)); } } throw lastError; } function createCacheKey(url, method, params) { const sortedParams = params ? Object.keys(params).sort().reduce((acc, key) => { acc[key] = params[key]; return acc; }, {}) : {}; return `${method}:${url}:${JSON.stringify(sortedParams)}`; } function parseNetSuiteError(error) { if (NetSuiteError.prototype.isPrototypeOf(error)) { return { message: error.message, code: error.code, details: error.details }; } if (error.response?.data) { const data = error.response.data; return { message: data.detail || data.message || "NetSuite API error", code: data["o:errorCode"] || data.code, details: data["o:errorDetails"] || data }; } return { message: error.message || "Unknown error", code: error.code, details: error }; } function formatNetSuiteDate(date) { return date.toISOString().split("T")[0]; } function parseNetSuiteDate(dateString) { return /* @__PURE__ */ new Date(dateString + "T00:00:00Z"); } function buildSearchQuery(filters) { return Object.entries(filters).filter(([_, value]) => value !== null && value !== void 0).map(([key, value]) => { if (Array.isArray(value)) { return `${key} IN (${value.map((v) => `'${v}'`).join(",")})`; } if (typeof value === "string") { return `${key} = '${value}'`; } return `${key} = ${value}`; }).join(" AND "); } function sanitizeFieldValue(value) { if (value === null || value === void 0) { return ""; } if (typeof value === "string") { return value.replace(/[\x00-\x1F\x7F]/g, "").trim(); } if (Array.isArray(value)) { return value.map(sanitizeFieldValue); } if (typeof value === "object") { const sanitized = {}; for (const [key, val] of Object.entries(value)) { sanitized[key] = sanitizeFieldValue(val); } return sanitized; } return value; } function toInternalId(id) { return String(id); } function validateConfig(config) { const errors = []; if (!config.oauth) { errors.push("OAuth configuration is required"); } else { if (!config.oauth.consumerKey) errors.push("OAuth consumer key is required"); if (!config.oauth.consumerSecret) errors.push("OAuth consumer secret is required"); if (!config.oauth.tokenKey) errors.push("OAuth token key is required"); if (!config.oauth.tokenSecret) errors.push("OAuth token secret is required"); if (!config.oauth.realm) errors.push("OAuth realm is required"); } if (!config.accountId) { errors.push("Account ID is required"); } return errors; } export { NetSuiteClient, NetSuiteError, RateLimiter, RequestBatcher, ResponseCache, buildSearchQuery, createCacheKey, formatNetSuiteDate, parseNetSuiteDate, parseNetSuiteError, retryWithBackoff, sanitizeFieldValue, toInternalId, validateConfig }; //# sourceMappingURL=index.mjs.map //# sourceMappingURL=index.mjs.map