zdata-client
Version:
TypeScript client library for zdata backend API with authentication and full CRUD operations
512 lines • 17.5 kB
JavaScript
/**
* @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