Initial commit: Omada API TypeScript library
This commit is contained in:
182
src/client/OmadaClient.ts
Normal file
182
src/client/OmadaClient.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* Core API client for Omada Controller REST API
|
||||
*/
|
||||
|
||||
import https from 'https';
|
||||
import { Authentication, AuthConfig } from '../auth/Authentication.js';
|
||||
import {
|
||||
OmadaApiError,
|
||||
OmadaNetworkError,
|
||||
OmadaDeviceNotFoundError,
|
||||
OmadaConfigurationError,
|
||||
} from '../errors/OmadaErrors.js';
|
||||
import { ApiResponse, ApiRequestOptions, PaginatedResponse } from '../types/api.js';
|
||||
|
||||
export interface OmadaClientConfig extends AuthConfig {
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main client class for interacting with Omada Controller API
|
||||
*/
|
||||
export class OmadaClient {
|
||||
private auth: Authentication;
|
||||
private config: OmadaClientConfig;
|
||||
private httpsAgent: https.Agent;
|
||||
private siteId: string | null = null;
|
||||
|
||||
constructor(config: OmadaClientConfig) {
|
||||
this.config = config;
|
||||
this.auth = new Authentication(config);
|
||||
this.httpsAgent = new https.Agent({
|
||||
rejectUnauthorized: config.verifySSL ?? true,
|
||||
});
|
||||
this.siteId = config.siteId || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current site ID (auto-detect if not set)
|
||||
*/
|
||||
async getSiteId(): Promise<string> {
|
||||
if (this.siteId) {
|
||||
return this.siteId;
|
||||
}
|
||||
|
||||
// Auto-detect site ID by getting the default site
|
||||
const sites = await this.request<Array<{ id: string; name: string }>>('GET', '/sites');
|
||||
if (!sites || sites.length === 0) {
|
||||
throw new OmadaApiError('No sites found in Omada Controller');
|
||||
}
|
||||
|
||||
// Use the first site as default
|
||||
this.siteId = sites[0].id;
|
||||
return this.siteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the site ID explicitly
|
||||
*/
|
||||
setSiteId(siteId: string): void {
|
||||
this.siteId = siteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an authenticated API request
|
||||
*/
|
||||
async request<T = any>(
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
|
||||
endpoint: string,
|
||||
options: Omit<ApiRequestOptions, 'method'> = {}
|
||||
): Promise<T> {
|
||||
const token = await this.auth.getAccessToken();
|
||||
const url = this.buildUrl(endpoint, options.params);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
// Note: Native fetch in Node.js 18+ doesn't support agent directly
|
||||
// For SSL certificate handling, ensure verifySSL config is set correctly
|
||||
// @ts-expect-error - agent may not be supported by all fetch implementations
|
||||
agent: this.httpsAgent,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let data: ApiResponse<T>;
|
||||
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch (parseError) {
|
||||
throw new OmadaApiError(
|
||||
`Invalid JSON response: ${text.substring(0, 200)}`,
|
||||
response.status
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new OmadaApiError(
|
||||
data.msg || `HTTP ${response.status}: ${response.statusText}`,
|
||||
response.status,
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
// Check Omada API error code (0 = success)
|
||||
if (data.errorCode !== 0) {
|
||||
const errorMsg = data.msg || `API error code: ${data.errorCode}`;
|
||||
|
||||
// Handle specific error cases
|
||||
if (data.errorCode === 10001) {
|
||||
// Token expired or invalid
|
||||
this.auth.clearToken();
|
||||
throw new OmadaApiError('Authentication token expired', 401, data);
|
||||
}
|
||||
|
||||
if (data.errorCode === 10002 || data.errorCode === 10003) {
|
||||
throw new OmadaDeviceNotFoundError(endpoint);
|
||||
}
|
||||
|
||||
if (data.errorCode >= 10000 && data.errorCode < 20000) {
|
||||
throw new OmadaConfigurationError(errorMsg, data);
|
||||
}
|
||||
|
||||
throw new OmadaApiError(errorMsg, response.status, data);
|
||||
}
|
||||
|
||||
return data.result as T;
|
||||
} catch (error) {
|
||||
if (error instanceof OmadaApiError || error instanceof OmadaDeviceNotFoundError || error instanceof OmadaConfigurationError) {
|
||||
throw error;
|
||||
}
|
||||
throw new OmadaNetworkError(
|
||||
`Request failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
error instanceof Error ? error : undefined
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a paginated API request
|
||||
*/
|
||||
async requestPaginated<T = any>(
|
||||
method: 'GET' | 'POST',
|
||||
endpoint: string,
|
||||
options: Omit<ApiRequestOptions, 'method'> = {}
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
const result = await this.request<PaginatedResponse<T>>(method, endpoint, options);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full URL with query parameters
|
||||
*/
|
||||
private buildUrl(endpoint: string, params?: Record<string, string | number | boolean>): string {
|
||||
const baseUrl = this.config.baseUrl.replace(/\/$/, '');
|
||||
let url = `${baseUrl}${endpoint.startsWith('/') ? endpoint : `/${endpoint}`}`;
|
||||
|
||||
if (params && Object.keys(params).length > 0) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
searchParams.append(key, String(value));
|
||||
}
|
||||
url += `?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentication instance (for advanced use cases)
|
||||
*/
|
||||
getAuth(): Authentication {
|
||||
return this.auth;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user