UNPKG

zdata-client

Version:

TypeScript client library for zdata backend API with authentication and full CRUD operations

512 lines 17.5 kB
/** * @fileoverview zdata API Client implementation */ import axios, {} from "axios"; import { ApiClientError, InvalidCredentialsError, ValidationError, } from "./types.js"; // ============================================================================= // API CLIENT IMPLEMENTATION // ============================================================================= /** * zdata API Client for handling authentication and CRUD operations * * This client provides a complete interface to the zdata backend API, * including user authentication, token management, and full CRUD operations * for any resource with automatic error handling and type safety. * * @example * ```typescript * import { ZDataClient } from 'zdata-client'; * * const client = new ZDataClient({ * baseUrl: 'https://api.example.com', * workspaceId: 'workspace-123' * }); * * // Authentication * await client.login({ * email: 'user@example.com', * password: 'password' * }); * * // CRUD operations * const users = await client.findRecords({ * resourceName: 'users', * page: 1, * limit: 10, * search: 'john' * }); * * const newUser = await client.createRecord('users', { * name: 'John Doe', * email: 'john@example.com' * }); * // newUser will have id, created_at, updated_at fields added * ``` */ export class ZDataClient { /** * Create a new zdata API client instance * @param config - Client configuration options * @throws {Error} When configuration is invalid */ constructor(config) { this.accessToken = null; this.validateConfig(config); this.httpClient = this.createHttpClient(config); this.setupInterceptors(); } // ============================================================================= // AUTHENTICATION METHODS // ============================================================================= /** * Authenticate user with email and password * * @param credentials - User login credentials * @returns Promise resolving to authentication response with user data and token * @throws {InvalidCredentialsError} When email/password combination is invalid * @throws {ValidationError} When request data is malformed * @throws {ApiClientError} When API request fails * * @example * ```typescript * try { * const auth = await client.login({ * email: 'user@example.com', * password: 'securePassword123' * }); * console.log('Logged in as:', auth.user.name); * } catch (error) { * if (error instanceof InvalidCredentialsError) { * console.error('Invalid email or password'); * } * } * ``` */ async login(credentials) { this.validateLoginCredentials(credentials); const response = await this.makeRequest({ method: "post", endpoint: "/login", data: credentials, }); this.setAccessToken(response.access_token); return response; } /** * Register a new user account * * @param userData - User registration data * @returns Promise resolving to authentication response with user data and token * @throws {ValidationError} When registration data is invalid * @throws {ApiClientError} When API request fails (e.g., email already exists) * * @example * ```typescript * try { * const auth = await client.register({ * name: 'John Doe', * email: 'john@example.com', * password: 'securePassword123' * }); * console.log('Registered user:', auth.user.name); * } catch (error) { * if (error instanceof ValidationError) { * console.error('Validation errors:', error.errors); * } * } * ``` */ async register(userData) { this.validateRegisterData(userData); return this.makeRequest({ method: "post", endpoint: "/register", data: userData, }); } /** * Clear authentication token and log out user * * @example * ```typescript * client.logout(); * console.log('User logged out'); * ``` */ logout() { this.accessToken = null; } /** * Check if user is currently authenticated * * @returns True if user has a valid access token * * @example * ```typescript * if (client.isAuthenticated()) { * console.log('User is logged in'); * } else { * console.log('User needs to log in'); * } * ``` */ isAuthenticated() { return Boolean(this.accessToken); } // ============================================================================= // CRUD METHODS // ============================================================================= /** * Create a new record in the specified resource * * @template T - The entity type to create * @param resourceName - Name of the resource to create record in * @param data - Record data to create (without base entity fields) * @returns Promise resolving to the created record with base entity fields * @throws {ValidationError} When record data is invalid * @throws {ApiClientError} When API request fails * * @example * ```typescript * interface User { * name: string; * email: string; * } * * const newUser = await client.createRecord<User>('users', { * name: 'John Doe', * email: 'john@example.com' * }); * // newUser will have id, created_at, updated_at fields added * ``` */ async createRecord(resourceName, data) { this.validateResourceName(resourceName); if (!data || typeof data !== "object") { throw new ValidationError("Record data must be a valid object"); } return this.makeRequest({ method: "post", endpoint: `/${resourceName}`, data, }); } /** * Update an existing record * * @template T - The entity type to update * @param resourceName - Name of the resource * @param id - Unique identifier of the record to update * @param data - Partial record data to update (without base entity fields) * @returns Promise resolving to the updated record with base entity fields * @throws {ValidationError} When record data is invalid * @throws {ApiClientError} When API request fails * * @example * ```typescript * interface User { * name: string; * email: string; * } * * const updatedUser = await client.updateRecord<User>('users', 'user-123', { * name: 'John Smith' * }); * // updatedUser will have all fields including updated updated_at * ``` */ async updateRecord(resourceName, id, data) { this.validateResourceName(resourceName); this.validateId(id); if (!data || typeof data !== "object") { throw new ValidationError("Record data must be a valid object"); } return this.makeRequest({ method: "put", endpoint: `/${resourceName}/${id}`, data, }); } /** * Delete a record by ID * * @param resourceName - Name of the resource * @param id - Record identifier * @returns Promise that resolves when deletion is complete * @throws {ApiClientError} When record is not found or API request fails * * @example * ```typescript * await client.deleteRecord('users', 'user-123'); * console.log('User deleted successfully'); * ``` */ async deleteRecord(resourceName, id) { this.validateResourceName(resourceName); this.validateId(id); await this.makeRequest({ method: "delete", endpoint: `/${resourceName}/${id}`, }); } /** * Find a specific record by ID * * @template T - The entity type to return * @param resourceName - Name of the resource * @param id - Record identifier * @returns Promise resolving to the found record with base entity fields * @throws {ApiClientError} When record is not found or API request fails * * @example * ```typescript * interface User { * name: string; * email: string; * } * * const user = await client.findRecordById<User>('users', 'user-123'); * // user will have id, created_at, updated_at fields included * console.log('Found user:', user.name); * ``` */ async findRecordById(resourceName, id) { this.validateResourceName(resourceName); this.validateId(id); return this.makeRequest({ method: "get", endpoint: `/${resourceName}/${id}`, }); } /** * Find records with pagination and optional search * * @template T - The entity type to return * @param params - Query parameters including resource name, pagination, and search * @returns Promise resolving to paginated response with records containing base entity fields * @throws {ApiClientError} When API request fails * * @example * ```typescript * interface User { * name: string; * email: string; * } * * const result = await client.findRecords<User>({ * resourceName: 'users', * page: 1, * limit: 10, * search: 'john' * }); * * console.log(`Found ${result.meta.totalRecords} users`); * result.records.forEach(user => { * // user has id, created_at, updated_at fields included * console.log(user.name, user.id); * }); * * if (result.meta.hasNext) { * console.log('More results available'); * } * ``` */ async findRecords(params) { this.validateResourceName(params.resourceName); const searchParams = this.buildSearchParams(params); return this.makeRequest({ method: "get", endpoint: `/${params.resourceName}`, searchParams, }); } // ============================================================================= // TOKEN MANAGEMENT // ============================================================================= /** * Set the access token for authentication * * This method allows manual token management, useful when implementing * custom authentication flows or token persistence. * * @param token - JWT access token * @throws {Error} When token is invalid or empty * * @example * ```typescript * // Set token from external source * const savedToken = localStorage.getItem('authToken'); * if (savedToken) { * client.setAccessToken(savedToken); * } * ``` */ setAccessToken(token) { if (!token || typeof token !== "string") { throw new Error("Invalid access token: must be a non-empty string"); } this.accessToken = token; } /** * Get the current access token * * @returns Current access token or null if not authenticated * * @example * ```typescript * const token = client.getAccessToken(); * if (token) { * localStorage.setItem('authToken', token); * } * ``` */ getAccessToken() { return this.accessToken; } // ============================================================================= // PRIVATE VALIDATION METHODS // ============================================================================= validateConfig(config) { if (!config.baseUrl) { throw new Error("Configuration error: baseUrl is required"); } if (!config.workspaceId) { throw new Error("Configuration error: workspaceId is required"); } if (config.timeout !== undefined && (config.timeout <= 0 || config.timeout > 300000)) { throw new Error("Configuration error: timeout must be between 1 and 300000 milliseconds"); } } validateLoginCredentials(credentials) { if (!credentials.email) { throw new Error("Email is required for login"); } if (!credentials.password) { throw new Error("Password is required for login"); } if (typeof credentials.email !== "string" || typeof credentials.password !== "string") { throw new Error("Email and password must be strings"); } } validateRegisterData(userData) { if (!userData.name) { throw new Error("Name is required for registration"); } if (!userData.email) { throw new Error("Email is required for registration"); } if (!userData.password) { throw new Error("Password is required for registration"); } if (typeof userData.name !== "string" || typeof userData.email !== "string" || typeof userData.password !== "string") { throw new Error("Name, email, and password must be strings"); } } validateResourceName(resourceName) { if (!resourceName || typeof resourceName !== "string") { throw new Error("Resource name is required and must be a non-empty string"); } if (resourceName.trim() !== resourceName) { throw new Error("Resource name cannot have leading or trailing whitespace"); } } validateId(id) { if (!id || typeof id !== "string") { throw new Error("ID is required and must be a non-empty string"); } if (id.trim() !== id) { throw new Error("ID cannot have leading or trailing whitespace"); } } // ============================================================================= // PRIVATE HTTP CLIENT METHODS // ============================================================================= createHttpClient(config) { return axios.create({ baseURL: `${config.baseUrl}/api/v1/${config.workspaceId}`, timeout: config.timeout ?? 10000, headers: { "Content-Type": "application/json", Accept: "application/json", ...config.headers, }, }); } setupInterceptors() { // Request interceptor - add auth token this.httpClient.interceptors.request.use((config) => { if (this.accessToken && config.headers) { config.headers.Authorization = `Bearer ${this.accessToken}`; } return config; }, (error) => Promise.reject(error)); // Response interceptor - handle errors this.httpClient.interceptors.response.use((response) => response, (error) => { throw this.handleHttpError(error); }); } handleHttpError(error) { if (!error.response) { return new ApiClientError("Network error: Unable to connect to the server"); } const { status, data } = error.response; const apiError = data; switch (status) { case 401: return new InvalidCredentialsError(apiError?.message ?? "Authentication failed: Invalid credentials"); case 400: return new ValidationError(apiError?.message ?? "Validation failed: Invalid request data", apiError?.errors ?? []); case 404: return new ApiClientError("Resource not found", 404); case 429: return new ApiClientError("Rate limit exceeded: Too many requests", 429); case 500: return new ApiClientError("Internal server error: Please try again later", 500); case 503: return new ApiClientError("Service unavailable: Server is temporarily down", 503); default: return new ApiClientError(apiError?.message ?? `HTTP error ${status}: Request failed`, status); } } buildSearchParams(params) { const searchParams = { page: String(params.page ?? 1), limit: String(params.limit ?? 10), }; if (params.search) { searchParams.search = params.search; } return searchParams; } async makeRequest(params) { try { let response; if (params.method === "get" || params.method === "delete") { response = await this.httpClient[params.method](params.endpoint, { params: params.searchParams, }); } else { response = await this.httpClient[params.method](params.endpoint, params.data, { params: params.searchParams }); } return response.data; } catch (error) { // Error is already handled by interceptor throw error; } } } // ============================================================================= // CONVENIENCE EXPORTS // ============================================================================= /** * Legacy alias for backward compatibility * @deprecated Use ZDataClient instead */ export const ExternalApiClient = ZDataClient; //# sourceMappingURL=client.js.map