import { smartlog, smartrequest } from './plugins.js'; import { type IPeeringDbResponse, type THttpMethod, type IQueryOptions, } from './peeringdb.types.js'; // Import manager classes (will be created next) import { OrganizationManager } from './peeringdb.classes.organizationmanager.js'; import { NetworkManager } from './peeringdb.classes.networkmanager.js'; import { FacilityManager } from './peeringdb.classes.facilitymanager.js'; import { ExchangeManager } from './peeringdb.classes.exchangemanager.js'; import { NetIxLanManager } from './peeringdb.classes.netixlanmanager.js'; import { NetFacManager } from './peeringdb.classes.netfacmanager.js'; import { IxLanManager } from './peeringdb.classes.ixlanmanager.js'; import { IxFacManager } from './peeringdb.classes.ixfacmanager.js'; import { IxPfxManager } from './peeringdb.classes.ixpfxmanager.js'; import { PocManager } from './peeringdb.classes.pocmanager.js'; /** * PeeringDB API Client * Provides access to the PeeringDB API using native fetch */ export class PeeringDbClient { private apiKey: string | null = null; private baseUrl: string = 'https://www.peeringdb.com/api'; private logger: smartlog.Smartlog; // Manager instances public organizations: OrganizationManager; public networks: NetworkManager; public facilities: FacilityManager; public exchanges: ExchangeManager; public netIxLans: NetIxLanManager; public netFacs: NetFacManager; public ixLans: IxLanManager; public ixFacs: IxFacManager; public ixPfxs: IxPfxManager; public pocs: PocManager; /** * Create a new PeeringDB API client * @param apiKey Optional API key for authenticated requests */ constructor(apiKey?: string) { this.apiKey = apiKey || null; this.logger = new smartlog.Smartlog({ logContext: { company: 'Task Venture Capital', companyunit: '@apiclient.xyz/peeringdb', containerName: 'PeeringDbClient', }, }); // Initialize managers this.organizations = new OrganizationManager(this); this.networks = new NetworkManager(this); this.facilities = new FacilityManager(this); this.exchanges = new ExchangeManager(this); this.netIxLans = new NetIxLanManager(this); this.netFacs = new NetFacManager(this); this.ixLans = new IxLanManager(this); this.ixFacs = new IxFacManager(this); this.ixPfxs = new IxPfxManager(this); this.pocs = new PocManager(this); } /** * Make a request to the PeeringDB API * @param endpoint API endpoint (e.g., 'net', 'org', 'fac') * @param method HTTP method * @param options Query options and parameters * @param body Request body for POST/PUT/PATCH * @returns Array of results (unwrapped from meta wrapper) */ public async request( endpoint: string, method: THttpMethod = 'GET', options: IQueryOptions = {}, body?: any ): Promise { const url = this.buildUrl(endpoint, options); this.logger.log('info', `${method} ${url}`); // Build request using fluent API let requestClient = smartrequest.SmartRequestClient.create() .url(url) .header('Accept', 'application/json') .header('User-Agent', '@apiclient.xyz/peeringdb/1.0.1 (Node.js)') .retry(3); // Retry up to 3 times (handles 429) // Add API key if available if (this.apiKey) { requestClient = requestClient.header('Authorization', `Api-Key ${this.apiKey}`); } // Add body for POST/PUT/PATCH requests if (body && ['POST', 'PUT', 'PATCH'].includes(method)) { requestClient = requestClient.json(body); } try { // Execute the appropriate HTTP method let response; switch (method) { case 'GET': response = await requestClient.get(); break; case 'POST': response = await requestClient.post(); break; case 'PUT': response = await requestClient.put(); break; case 'DELETE': response = await requestClient.delete(); break; case 'PATCH': response = await requestClient.patch(); break; default: throw new Error(`Unsupported HTTP method: ${method}`); } // Response body is automatically parsed as JSON by smartrequest const data: IPeeringDbResponse = response.body; // Check meta status if (data?.meta?.status === 'error') { this.logger.log('error', `API returned error: ${data.meta.message}`); throw new Error(`PeeringDB API error: ${data.meta.message}`); } // Handle pagination if autoPaginate is enabled if (options.autoPaginate && data.data.length === (options.limit || 0)) { const allResults = [...data.data]; let currentSkip = (options.skip || 0) + data.data.length; while (true) { const nextOptions = { ...options, skip: currentSkip }; const nextUrl = this.buildUrl(endpoint, nextOptions); const nextResponse = await smartrequest.SmartRequestClient.create() .url(nextUrl) .header('Accept', 'application/json') .header('User-Agent', '@apiclient.xyz/peeringdb/1.0.1 (Node.js)') .header('Authorization', this.apiKey ? `Api-Key ${this.apiKey}` : '') .retry(3) .get(); const nextData: IPeeringDbResponse = nextResponse.body; if (nextData.data.length === 0) { break; } allResults.push(...nextData.data); currentSkip += nextData.data.length; // Safety check to prevent infinite loops if (nextData.data.length < (options.limit || 0)) { break; } } return allResults; } // Return unwrapped data array return data.data; } catch (error) { this.logger.log('error', `Request failed: ${error.message}`); throw error; } } /** * Build API URL with query parameters */ private buildUrl(endpoint: string, options: IQueryOptions = {}): string { const url = new URL(`${this.baseUrl}/${endpoint}`); // Add standard query parameters if (options.limit !== undefined) { url.searchParams.set('limit', options.limit.toString()); } if (options.skip !== undefined) { url.searchParams.set('skip', options.skip.toString()); } if (options.fields) { url.searchParams.set('fields', options.fields); } if (options.depth !== undefined) { url.searchParams.set('depth', options.depth.toString()); } if (options.since !== undefined) { url.searchParams.set('since', options.since.toString()); } // Add any additional query parameters (field filters, etc.) Object.keys(options).forEach((key) => { if ( !['limit', 'skip', 'fields', 'depth', 'since', 'autoPaginate'].includes(key) ) { const value = options[key]; if (value !== undefined && value !== null) { url.searchParams.set(key, value.toString()); } } }); return url.toString(); } /** * Convenience methods for common operations */ public convenience = { /** * Get a network by ASN */ getNetworkByAsn: async (asn: number) => { const results = await this.request('net', 'GET', { asn }); return results[0] || null; }, /** * Get an organization by ID */ getOrganizationById: async (id: number) => { return this.organizations.getById(id); }, /** * Get a facility by ID */ getFacilityById: async (id: number) => { return this.facilities.getById(id); }, /** * Get an exchange by ID */ getExchangeById: async (id: number) => { return this.exchanges.getById(id); }, /** * Search networks by name */ searchNetworks: async (query: string) => { return this.request('net', 'GET', { name__contains: query }); }, /** * Search facilities by name */ searchFacilities: async (query: string) => { return this.request('fac', 'GET', { name__contains: query }); }, /** * Get all facilities where a network is present */ getNetworkFacilities: async (asn: number) => { const network = await this.convenience.getNetworkByAsn(asn); if (!network) { return []; } return this.request('netfac', 'GET', { net_id: network.id, depth: 2 }); }, /** * Get all exchanges where a network peers */ getNetworkExchanges: async (asn: number) => { const network = await this.convenience.getNetworkByAsn(asn); if (!network) { return []; } return this.request('netixlan', 'GET', { net_id: network.id, depth: 2 }); }, /** * Get all networks present at a facility */ getFacilityNetworks: async (facId: number) => { return this.request('netfac', 'GET', { fac_id: facId, depth: 2 }); }, }; }