JSPM

zdata-client

1.1.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 2
  • Score
    100M100P100Q44872F
  • License MIT

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

Package Exports

  • zdata-client

Readme

zdata-client

A modern, type-safe TypeScript client library for the zdata backend API. Provides authentication, CRUD operations, and comprehensive error handling with excellent developer experience.

🚀 Features

  • Type-Safe: Full TypeScript support with comprehensive type definitions
  • Authentication: JWT-based login/register with automatic token management
  • CRUD Operations: Complete Create, Read, Update, Delete operations for any resource
  • Error Handling: Custom error classes with detailed error information
  • Pagination: Built-in pagination support with metadata
  • Search: Integrated search functionality across resources
  • Modern: ES modules, async/await, and modern JavaScript patterns
  • Zero Dependencies: Only requires axios for HTTP requests
  • Generic Types: For type-safe data access
  • Extensible Base Classes: For custom data sources
  • Request/Response Validation:

📦 Installation

npm install zdata-client
yarn add zdata-client
pnpm add zdata-client

🛠️ Quick Start

Basic Setup

import { ZDataClient } from "zdata-client";

const client = new ZDataClient({
  baseUrl: "https://api.yourdomain.com",
  workspaceId: "your-workspace-id",
  timeout: 10000, // optional, default: 10000ms
  headers: {
    // optional
    "Custom-Header": "value",
  },
});

Alternative Setup (Factory Function)

import { createClient } from "zdata-client";

const client = createClient({
  baseUrl: "https://api.yourdomain.com",
  workspaceId: "your-workspace-id",
});

🔐 Authentication

Login

try {
  const auth = await client.login({
    email: "user@example.com",
    password: "your-password",
  });

  console.log("Welcome,", auth.user.name);
  console.log("Token expires in:", auth.expires_in, "seconds");
} catch (error) {
  if (error instanceof InvalidCredentialsError) {
    console.error("Invalid email or password");
  }
}

Register

try {
  const auth = await client.register({
    name: "John Doe",
    email: "john@example.com",
    password: "secure-password",
  });

  console.log("Account created for:", auth.user.name);
} catch (error) {
  if (error instanceof ValidationError) {
    console.error("Validation errors:", error.errors);
  }
}

Token Management

// Manual token management
const token = localStorage.getItem("authToken");
if (token) {
  client.setAccessToken(token);
}

// Save token after login
const auth = await client.login(credentials);
localStorage.setItem("authToken", auth.access_token);

// Check authentication status
if (client.isAuthenticated()) {
  console.log("User is logged in");
}

// Logout
client.logout();

📋 CRUD Operations

Create Records

const newUser = await client.createRecord("users", {
  name: "Jane Smith",
  email: "jane@example.com",
  role: "admin",
});

console.log("Created user:", newUser);

Read Records

Find by ID

const user = await client.findRecordById("users", "user-123");
console.log("User details:", user);
const result = await client.findRecords({
  resourceName: "users",
  page: 1,
  limit: 10,
  search: "john", // optional search query
});

console.log(`Found ${result.meta.totalRecords} users`);
console.log(
  `Page ${result.meta.activePageNumber} of ${result.meta.totalPages}`
);

result.records.forEach((user) => {
  console.log("User:", user.name);
});

// Pagination info
if (result.meta.hasNext) {
  console.log("More results available");
}

Update Records

const updatedUser = await client.updateRecord("users", "user-123", {
  name: "Jane Doe",
  role: "superadmin",
});

console.log("Updated user:", updatedUser);

Delete Records

await client.deleteRecord("users", "user-123");
console.log("User deleted successfully");

🎯 Type-Safe Operations with Generics

Using Generic Types

The client supports TypeScript generics for type-safe operations:

interface User {
  name: string;
  email: string;
  role: "admin" | "user";
}

// Type-safe operations
const newUser = await client.createRecord<User>("users", {
  name: "John Doe",
  email: "john@example.com",
  role: "user",
  // TypeScript will ensure you don't include id, created_at, updated_at
});

// Response automatically includes base entity fields
console.log(newUser.id); // ✅ Available (string)
console.log(newUser.created_at); // ✅ Available (string)
console.log(newUser.name); // ✅ Available (string)

// Type-safe queries
const user = await client.findRecordById<User>("users", "user-123");
const users = await client.findRecords<User>({ resourceName: "users" });

Entity Type Utilities

import { CreateEntity, EntityWithBase } from "zdata-client";

interface Product {
  name: string;
  price: number;
  category: string;
}

// For creation (excludes id, created_at, updated_at)
type ProductInput = CreateEntity<Product>;
// Result: { name: string; price: number; category: string; }

// For responses (includes id, created_at, updated_at)
type ProductOutput = EntityWithBase<Product>;
// Result: { id: string; created_at: string; updated_at: string; name: string; price: number; category: string; }

const productData: ProductInput = {
  name: "Laptop",
  price: 999.99,
  category: "Electronics",
  // ❌ TypeScript error if you try to include id, created_at, updated_at
};

const savedProduct: ProductOutput = await client.createRecord<Product>(
  "products",
  productData
);

Custom Data Source Clients

Creating Custom Clients

Extend BaseDataSourceClient for type-safe, resource-specific clients:

import {
  BaseDataSourceClient,
  type CreateEntity,
  type EntityWithBase,
} from "zdata-client";

interface Payment {
  amount: number;
  description: string;
  userId: string;
  status: "pending" | "completed" | "failed";
  currency: string;
}

class PaymentClient extends BaseDataSourceClient<Payment> {
  constructor(config: ApiConfig) {
    super(config, "pagamentos"); // Resource name
  }

  // Simplified, type-safe methods
  findPayment = (id: string): Promise<EntityWithBase<Payment>> =>
    this.findById(id);

  deletePayment = (id: string): Promise<void> => this.delete(id);

  insertPayment = (
    data: CreateEntity<Payment>
  ): Promise<EntityWithBase<Payment>> => this.create(data);

  updatePayment = (
    id: string,
    data: Partial<CreateEntity<Payment>>
  ): Promise<EntityWithBase<Payment>> => this.update(id, data);

  findPayments = (params = {}) => this.find(params);

  // Custom business logic
  async findPaymentsByUser(userId: string) {
    return this.find({ search: `userId:${userId}` });
  }

  async findPendingPayments() {
    return this.find({ search: "status:pending" });
  }

  async markAsCompleted(id: string) {
    return this.update(id, { status: "completed" });
  }

  async getTotalAmountByUser(userId: string): Promise<number> {
    const payments = await this.findPaymentsByUser(userId);
    return payments.records.reduce(
      (total, payment) => total + payment.amount,
      0
    );
  }
}

Using Custom Clients

const paymentClient = new PaymentClient({
  baseUrl: "https://api.example.com",
  workspaceId: "workspace-123",
});

// Login once, works for all operations
await paymentClient.login({ email: "user@example.com", password: "password" });

// Type-safe operations
const payment = await paymentClient.insertPayment({
  amount: 100.5,
  description: "Monthly subscription",
  userId: "user-123",
  status: "pending",
  currency: "USD",
  // No need to specify id, created_at, updated_at - they're added automatically
});

console.log("Created payment:", payment.id, payment.created_at);

// Custom business logic
const userPayments = await paymentClient.findPaymentsByUser("user-123");
const pendingPayments = await paymentClient.findPendingPayments();
const totalAmount = await paymentClient.getTotalAmountByUser("user-123");

// Mark payment as completed
await paymentClient.markAsCompleted(payment.id);

Simple Data Source Client

For basic cases without custom logic, use DataSourceClient:

import { DataSourceClient } from "zdata-client";

interface User {
  name: string;
  email: string;
  role: "admin" | "user";
}

const userClient = new DataSourceClient<User>(config, "users");

// Direct usage with type safety
const newUser = await userClient.create({
  name: "Jane Doe",
  email: "jane@example.com",
  role: "user",
});

const users = await userClient.find({ page: 1, limit: 10 });
const user = await userClient.findById("user-123");
await userClient.update("user-123", { role: "admin" });
await userClient.delete("user-123");

🔍 Working with Different Resources

The client works with any resource in your zdata backend:

// Products
const products = await client.findRecords({ resourceName: "products" });
const product = await client.createRecord("products", { name: "New Product" });

// Orders
const orders = await client.findRecords({ resourceName: "orders", limit: 20 });
const order = await client.findRecordById("orders", "order-456");

// Custom resources
const customData = await client.findRecords({
  resourceName: "custom-entities",
});

🚨 Error Handling

The library provides custom error classes for different scenarios:

import {
  InvalidCredentialsError,
  ValidationError,
  ApiClientError,
  isValidationError,
  isApiClientError,
} from "zdata-client";

try {
  await client.createRecord("users", invalidData);
} catch (error) {
  if (isValidationError(error)) {
    console.error("Validation failed:");
    error.errors.forEach((err) => {
      console.error(`- ${err.message} at ${err.path.join(".")}`);
    });
  } else if (isApiClientError(error)) {
    console.error(`API Error (${error.statusCode}):`, error.message);
  } else {
    console.error("Unexpected error:", error.message);
  }
}

Error Types

  • InvalidCredentialsError: Authentication failed (401)
  • ValidationError: Request data validation failed (400)
  • ApiClientError: General API errors (404, 500, etc.)

🔧 Advanced Usage

Custom Headers

const client = new ZDataClient({
  baseUrl: "https://api.yourdomain.com",
  workspaceId: "workspace-id",
  headers: {
    "X-Custom-Header": "custom-value",
    "X-Client-Version": "1.0.0",
  },
});

Request Timeout

const client = new ZDataClient({
  baseUrl: "https://api.yourdomain.com",
  workspaceId: "workspace-id",
  timeout: 30000, // 30 seconds
});

Pagination Helper

async function getAllUsers() {
  const allUsers = [];
  let page = 1;
  let hasMore = true;

  while (hasMore) {
    const result = await client.findRecords({
      resourceName: "users",
      page,
      limit: 100,
    });

    allUsers.push(...result.records);
    hasMore = result.meta.hasNext;
    page++;
  }

  return allUsers;
}

📚 API Reference

Client Configuration

interface ApiConfig {
  baseUrl: string; // API base URL
  workspaceId: string; // Workspace identifier
  timeout?: number; // Request timeout (default: 10000ms)
  headers?: Record<string, string>; // Custom headers
}

Authentication Methods

  • login(credentials: LoginRequest): Promise<AuthResponse>
  • register(userData: RegisterRequest): Promise<AuthResponse>
  • logout(): void
  • isAuthenticated(): boolean
  • setAccessToken(token: string): void
  • getAccessToken(): string | null

CRUD Methods

  • createRecord(resourceName: string, data: unknown): Promise<unknown>
  • updateRecord(resourceName: string, id: string, data: unknown): Promise<unknown>
  • deleteRecord(resourceName: string, id: string): Promise<void>
  • findRecordById(resourceName: string, id: string): Promise<unknown>
  • findRecords(params: FindRecordsParams): Promise<PaginatedResponse>

Response Types

interface PaginatedResponse<T = unknown> {
  records: T[];
  meta: {
    activePageNumber: number;
    limit: number;
    totalRecords: number;
    totalPages: number;
    hasNext: boolean;
    hasPrev: boolean;
  };
}

🌟 Best Practices

1. Environment Configuration

// config.ts
export const apiConfig = {
  baseUrl: process.env.VITE_API_BASE_URL || "https://api.yourdomain.com",
  workspaceId: process.env.VITE_WORKSPACE_ID || "default-workspace",
};

// app.ts
import { ZDataClient } from "zdata-client";
import { apiConfig } from "./config";

const client = new ZDataClient(apiConfig);

2. Error Boundary

class ApiService {
  private client: ZDataClient;

  constructor(config: ApiConfig) {
    this.client = new ZDataClient(config);
  }

  async getUsers(page = 1, search?: string) {
    try {
      return await this.client.findRecords({
        resourceName: "users",
        page,
        limit: 10,
        search,
      });
    } catch (error) {
      console.error("Failed to fetch users:", error);
      throw error;
    }
  }
}

3. Token Persistence

class AuthManager {
  private client: ZDataClient;

  constructor(client: ZDataClient) {
    this.client = client;
    this.loadStoredToken();
  }

  private loadStoredToken() {
    const token = localStorage.getItem("authToken");
    if (token) {
      this.client.setAccessToken(token);
    }
  }

  async login(credentials: LoginRequest) {
    const auth = await this.client.login(credentials);
    localStorage.setItem("authToken", auth.access_token);
    return auth;
  }

  logout() {
    this.client.logout();
    localStorage.removeItem("authToken");
  }
}

📄 License

MIT License - see the LICENSE file for details.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

📧 Support

For questions and support, please open an issue in the GitHub repository.