Practical Application Examples
This document provides practical examples of using the Lovrabet SDK in real projects, including integration solutions for React, Vue, Node.js, and other environments.
Prerequisites
Before starting, make sure you have completed SDK Configuration. It's recommended to use CLI auto-generated configuration.
The following examples use the alias approach client.models.users for better readability. You can also use the standard approach client.models.dataset_xxx (functionality is identical).
⚛️ React Application Integration
Project Structure Setup
src/
├── api/
│ ├── config.ts # API configuration
│ ├── client.ts # Client instance
│ └── types.ts # Type definitions
├── hooks/
│ ├── useLovrabet.ts # Custom Hook
│ └── useAuth.ts # Auth Hook
├── components/
│ ├── UserList.tsx # User list component
│ ├── UserForm.tsx # User form component
│ └── Dashboard.tsx # Dashboard component
└── utils/
└── errorHandler.ts # Error handling
API Configuration
// src/api/config.ts
import { registerModels, type ModelsConfig } from "@lovrabet/sdk";
export const LOVRABET_CONFIG: ModelsConfig = {
appCode: process.env.REACT_APP_LOVRABET_APP_CODE!,
models: [
{
datasetCode: process.env.REACT_APP_USERS_DATASET_ID!,
tableName: "users",
alias: "users",
name: "User Management",
},
{
datasetCode: process.env.REACT_APP_ORDERS_DATASET_ID!,
tableName: "orders",
alias: "orders",
name: "Order Management",
},
{
datasetCode: process.env.REACT_APP_PRODUCTS_DATASET_ID!,
tableName: "products",
alias: "products",
name: "Product Management",
},
],
} as const;
// Register configuration
registerModels(LOVRABET_CONFIG);
// src/api/client.ts
import { createClient } from "@lovrabet/sdk";
import "./config"; // Import config to execute registration
// Create client instance
export const lovrabetClient = createClient({
options: {
timeout: 30000,
onError: (error) => {
console.error("Lovrabet API Error:", error);
// Global error handling
if (error.status === 401) {
// Clear user authentication state
localStorage.removeItem("auth_token");
window.location.href = "/login";
}
},
},
});
// Set authentication token
const token = localStorage.getItem("auth_token");
if (token) {
lovrabetClient.setToken(token);
}
Type Definitions
// src/api/types.ts
export interface User {
id: string;
name: string;
email: string;
status: "active" | "inactive" | "pending";
role: "admin" | "user" | "moderator";
avatar?: string;
createdAt: string;
updatedAt: string;
}
export interface Order {
id: string;
userId: string;
status: "pending" | "processing" | "completed" | "cancelled";
amount: number;
currency: string;
createdAt: string;
updatedAt: string;
}
export interface Product {
id: string;
name: string;
description?: string;
price: number;
inStock: boolean;
categoryId: string;
createdAt: string;
}
// Create and update types
export type CreateUserData = Omit<User, "id" | "createdAt" | "updatedAt">;
export type UpdateUserData = Partial<
Omit<User, "id" | "createdAt" | "updatedAt">
>;
Custom Hook
// src/hooks/useLovrabet.ts
import { useState, useEffect, useCallback } from "react";
import { lovrabetClient } from "../api/client";
import type { ListResponse, ListParams } from "@lovrabet/sdk";
interface UseModelListResult<T> {
data: T[] | null;
loading: boolean;
error: string | null;
pagination: {
current: number;
size: number;
total: number;
pages: number;
} | null;
refetch: (params?: ListParams) => Promise<void>;
loadMore: () => Promise<void>;
create: (data: any) => Promise<T>;
update: (id: string, data: any) => Promise<T>;
delete: (id: string) => Promise<void>;
}
export function useModelList<T>(
modelName: string,
initialParams: ListParams = {}
): UseModelListResult<T> {
const [data, setData] = useState<T[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [pagination, setPagination] =
useState<UseModelListResult<T>["pagination"]>(null);
const [params, setParams] = useState(initialParams);
const fetchData = useCallback(
async (requestParams: ListParams = {}, append = false) => {
if (!append) setLoading(true);
setError(null);
try {
const response: ListResponse<T> = await lovrabetClient.models[
modelName
].filter({
...params,
...requestParams,
});
const newData = response.tableData;
setData((prevData) =>
append ? [...(prevData || []), ...newData] : newData
);
setPagination({
current: response.currentPage,
size: response.pageSize,
total: response.total,
pages: Math.ceil(response.total / response.pageSize),
});
} catch (err: any) {
const errorMessage = err?.message || "Failed to fetch data";
setError(errorMessage);
console.error(`Error fetching ${modelName}:`, err);
} finally {
setLoading(false);
}
},
[modelName, params]
);
const refetch = useCallback(
(newParams?: ListParams) => {
if (newParams) {
setParams((prev) => ({ ...prev, ...newParams }));
}
return fetchData(newParams);
},
[fetchData]
);
const loadMore = useCallback(async () => {
if (!pagination || pagination.current >= pagination.pages) return;
await fetchData(
{ ...params, currentPage: pagination.current + 1 },
true // append mode
);
}, [fetchData, params, pagination]);
const create = useCallback(
async (createData: any): Promise<T> => {
const newItem = await lovrabetClient.models[modelName].create<T>(
createData
);
setData((prevData) => [newItem, ...(prevData || [])]);
// Update pagination info
setPagination((prev) =>
prev ? { ...prev, total: prev.total + 1 } : null
);
return newItem;
},
[modelName]
);
const update = useCallback(
async (id: string, updateData: any): Promise<T> => {
const updatedItem = await lovrabetClient.models[modelName].update<T>(
id,
updateData
);
setData((prevData) =>
prevData
? prevData.map((item) =>
(item as any).id === id ? updatedItem : item
)
: null
);
return updatedItem;
},
[modelName]
);
const deleteItem = useCallback(
async (id: string): Promise<void> => {
await lovrabetClient.models[modelName].delete(id);
setData((prevData) =>
prevData ? prevData.filter((item) => (item as any).id !== id) : null
);
// Update pagination info
setPagination((prev) =>
prev ? { ...prev, total: prev.total - 1 } : null
);
},
[modelName]
);
useEffect(() => {
fetchData();
}, [fetchData]);
return {
data,
loading,
error,
pagination,
refetch,
loadMore,
create,
update,
delete: deleteItem,
};
}
// Specific model Hooks
export const useUsers = (params?: ListParams) =>
useModelList<User>("users", params);
export const useOrders = (params?: ListParams) =>
useModelList<Order>("orders", params);
export const useProducts = (params?: ListParams) =>
useModelList<Product>("products", params);
React Component Examples
// src/components/UserList.tsx
import React, { useState } from "react";
import { useUsers } from "../hooks/useLovrabet";
import type { User, CreateUserData, UpdateUserData } from "../api/types";
interface UserListProps {
filter?: Partial<User>;
onUserSelect?: (user: User) => void;
}
export const UserList: React.FC<UserListProps> = ({ filter, onUserSelect }) => {
const {
data: users,
loading,
error,
pagination,
refetch,
create,
update,
delete: deleteUser,
} = useUsers(filter);
const [editingUser, setEditingUser] = useState<User | null>(null);
const [showCreateForm, setShowCreateForm] = useState(false);
const handleCreateUser = async (userData: CreateUserData) => {
try {
await create(userData);
setShowCreateForm(false);
} catch (error) {
console.error("Failed to create user:", error);
}
};
const handleUpdateUser = async (id: string, updates: UpdateUserData) => {
try {
await update(id, updates);
setEditingUser(null);
} catch (error) {
console.error("Failed to update user:", error);
}
};
const handleDeleteUser = async (id: string) => {
if (window.confirm("Are you sure you want to delete this user?")) {
try {
await deleteUser(id);
} catch (error) {
console.error("Failed to delete user:", error);
}
}
};
if (loading) return <div className="loading">Loading users...</div>;
if (error) return <div className="error">Error: {error}</div>;
if (!users?.length) return <div className="empty">No users found</div>;
return (
<div className="user-list">
<div className="header">
<h2>Users</h2>
<button onClick={() => setShowCreateForm(true)}>Add User</button>
</div>
{pagination && (
<div className="pagination-info">
Showing {users.length} of {pagination.total} users
</div>
)}
<div className="user-grid">
{users.map((user) => (
<div key={user.id} className="user-card">
<div className="user-info">
<h3>{user.name}</h3>
<p>{user.email}</p>
<span className={`status ${user.status}`}>{user.status}</span>
<span className={`role ${user.role}`}>{user.role}</span>
</div>
<div className="user-actions">
<button onClick={() => onUserSelect?.(user)}>View</button>
<button onClick={() => setEditingUser(user)}>Edit</button>
<button
onClick={() => handleDeleteUser(user.id)}
className="danger"
>
Delete
</button>
</div>
</div>
))}
</div>
{pagination && pagination.current < pagination.pages && (
<button
onClick={() => refetch({ currentPage: pagination.current + 1 })}
>
Load More
</button>
)}
{showCreateForm && (
<UserCreateModal
onSubmit={handleCreateUser}
onCancel={() => setShowCreateForm(false)}
/>
)}
{editingUser && (
<UserEditModal
user={editingUser}
onSubmit={(updates) => handleUpdateUser(editingUser.id, updates)}
onCancel={() => setEditingUser(null)}
/>
)}
</div>
);
};
// src/components/Dashboard.tsx
import React from "react";
import { useUsers, useOrders, useProducts } from "../hooks/useLovrabet";
export const Dashboard: React.FC = () => {
const { data: users, loading: usersLoading } = useUsers({ pageSize: 5 });
const { data: orders, loading: ordersLoading } = useOrders({ pageSize: 5 });
const { data: products, loading: productsLoading } = useProducts({
pageSize: 5,
});
return (
<div className="dashboard">
<h1>Dashboard</h1>
<div className="dashboard-grid">
<div className="dashboard-card">
<h2>Recent Users</h2>
{usersLoading ? (
<div>Loading...</div>
) : (
<div>
{users?.slice(0, 5).map((user) => (
<div key={user.id} className="dashboard-item">
{user.name} - {user.status}
</div>
))}
</div>
)}
</div>
<div className="dashboard-card">
<h2>Recent Orders</h2>
{ordersLoading ? (
<div>Loading...</div>
) : (
<div>
{orders?.slice(0, 5).map((order) => (
<div key={order.id} className="dashboard-item">
Order #{order.id} - ${order.amount}
</div>
))}
</div>
)}
</div>
<div className="dashboard-card">
<h2>Products</h2>
{productsLoading ? (
<div>Loading...</div>
) : (
<div>
{products?.slice(0, 5).map((product) => (
<div key={product.id} className="dashboard-item">
{product.name} - ${product.price}
</div>
))}
</div>
)}
</div>
</div>
</div>
);
};
🟢 Vue 3 Application Integration
Composition API Hook
// src/composables/useLovrabet.ts
import { ref, computed, onMounted, type Ref } from "vue";
import { lovrabetClient } from "../api/client";
import type { ListResponse, ListParams } from "@lovrabet/sdk";
export function useModelList<T>(
modelName: string,
initialParams: ListParams = {}
) {
const data: Ref<T[]> = ref([]);
const loading = ref(true);
const error = ref<string | null>(null);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const pagination = computed(() => ({
current: currentPage.value,
size: pageSize.value,
total: total.value,
pages: Math.ceil(total.value / pageSize.value),
}));
const fetchData = async (params: ListParams = {}) => {
loading.value = true;
error.value = null;
try {
const response: ListResponse<T> = await lovrabetClient.models[
modelName
].filter({
...initialParams,
...params,
});
data.value = response.tableData;
total.value = response.total;
currentPage.value = response.currentPage;
pageSize.value = response.pageSize;
} catch (err: any) {
error.value = err?.message || "Failed to fetch data";
console.error(`Error fetching ${modelName}:`, err);
} finally {
loading.value = false;
}
};
const create = async (createData: any): Promise<T> => {
const newItem = await lovrabetClient.models[modelName].create<T>(
createData
);
data.value.unshift(newItem);
total.value += 1;
return newItem;
};
const update = async (id: string, updateData: any): Promise<T> => {
const updatedItem = await lovrabetClient.models[modelName].update<T>(
id,
updateData
);
const index = data.value.findIndex((item: any) => item.id === id);
if (index !== -1) {
data.value[index] = updatedItem;
}
return updatedItem;
};
const remove = async (id: string): Promise<void> => {
await lovrabetClient.models[modelName].delete(id);
data.value = data.value.filter((item: any) => item.id !== id);
total.value -= 1;
};
const refetch = (params?: ListParams) => fetchData(params);
onMounted(() => fetchData());
return {
data: readonly(data),
loading: readonly(loading),
error: readonly(error),
pagination: readonly(pagination),
refetch,
create,
update,
remove,
};
}
// Specific model composables
export const useUsers = (params?: ListParams) => useModelList("users", params);
export const useOrders = (params?: ListParams) =>
useModelList("orders", params);
Vue Component Example
<!-- src/components/UserList.vue -->
<template>
<div class="user-list">
<div class="header">
<h2>Users</h2>
<button @click="showCreateModal = true">Add User</button>
</div>
<div v-if="loading" class="loading">Loading users...</div>
<div v-else-if="error" class="error">Error: {{ error }}</div>
<div v-else-if="!data.length" class="empty">No users found</div>
<div v-else class="user-grid">
<div v-for="user in data" :key="user.id" class="user-card">
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span :class="`status ${user.status}`">{{ user.status }}</span>
</div>
<div class="user-actions">
<button @click="$emit('userSelect', user)">View</button>
<button @click="editUser(user)">Edit</button>
<button @click="deleteUser(user.id)" class="danger">Delete</button>
</div>
</div>
</div>
<div v-if="pagination.pages > 1" class="pagination">
<button
v-for="page in pagination.pages"
:key="page"
@click="refetch({ currentPage: page })"
:class="{ active: page === pagination.current }"
>
{{ page }}
</button>
</div>
<!-- Create user modal -->
<UserCreateModal
v-if="showCreateModal"
@submit="handleCreateUser"
@cancel="showCreateModal = false"
/>
<!-- Edit user modal -->
<UserEditModal
v-if="editingUser"
:user="editingUser"
@submit="handleUpdateUser"
@cancel="editingUser = null"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsers } from "../composables/useLovrabet";
import type { User, CreateUserData, UpdateUserData } from "../api/types";
interface Props {
filter?: Partial<User>;
}
const props = defineProps<Props>();
const emit = defineEmits<{
userSelect: [user: User];
}>();
const { data, loading, error, pagination, refetch, create, update, remove } =
useUsers(props.filter);
const showCreateModal = ref(false);
const editingUser = ref<User | null>(null);
const handleCreateUser = async (userData: CreateUserData) => {
try {
await create(userData);
showCreateModal.value = false;
} catch (error) {
console.error("Failed to create user:", error);
}
};
const editUser = (user: User) => {
editingUser.value = user;
};
const handleUpdateUser = async (updates: UpdateUserData) => {
if (!editingUser.value) return;
try {
await update(editingUser.value.id, updates);
editingUser.value = null;
} catch (error) {
console.error("Failed to update user:", error);
}
};
const deleteUser = async (id: string) => {
if (confirm("Are you sure you want to delete this user?")) {
try {
await remove(id);
} catch (error) {
console.error("Failed to delete user:", error);
}
}
};
</script>
🚀 Node.js Server-side Integration
Express.js API Server
// src/server.ts
import express from "express";
import cors from "cors";
import { registerModels, createClient } from "@lovrabet/sdk";
// Configure Lovrabet SDK
registerModels({
appCode: process.env.LOVRABET_APP_CODE!,
models: [
{
datasetCode: process.env.USERS_DATASET_ID!,
tableName: "users",
alias: "users",
},
{
datasetCode: process.env.ORDERS_DATASET_ID!,
tableName: "orders",
alias: "orders",
},
],
});
// Create authenticated client
const lovrabetClient = createClient({
accessKey: process.env.LOVRABET_ACCESS_KEY!,
secretKey: process.env.LOVRABET_SECRET_KEY!,
});
const app = express();
app.use(cors());
app.use(express.json());
// User routes
app.get("/api/users", async (req, res) => {
try {
const { page = 1, size = 20, status } = req.query;
const params = {
currentPage: parseInt(page as string),
pageSize: parseInt(size as string),
...(status && { status }),
};
const users = await lovrabetClient.models.users.filter(params);
res.json({
success: true,
data: users.tableData,
pagination: {
current: users.currentPage,
size: users.pageSize,
total: users.total,
pages: Math.ceil(users.total / users.pageSize),
},
});
} catch (error: any) {
console.error("Error fetching users:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
app.post("/api/users", async (req, res) => {
try {
const userData = req.body;
// Data validation
if (!userData.name || !userData.email) {
return res.status(400).json({
success: false,
error: "Name and email are required",
});
}
const newUser = await lovrabetClient.models.users.create(userData);
res.status(201).json({
success: true,
data: newUser,
});
} catch (error: any) {
console.error("Error creating user:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
app.put("/api/users/:id", async (req, res) => {
try {
const { id } = req.params;
const updates = req.body;
const updatedUser = await lovrabetClient.models.users.update(id, updates);
res.json({
success: true,
data: updatedUser,
});
} catch (error: any) {
console.error("Error updating user:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
app.delete("/api/users/:id", async (req, res) => {
try {
const { id } = req.params;
await lovrabetClient.models.users.delete(id);
res.json({
success: true,
message: "User deleted successfully",
});
} catch (error: any) {
console.error("Error deleting user:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// Order analytics API
app.get("/api/analytics/orders", async (req, res) => {
try {
const { startDate, endDate, userId } = req.query;
const params = {
currentPage: 1,
pageSize: 1000, // Get more data for analytics
...(userId && { userId }),
...(startDate && { createdAfter: startDate }),
...(endDate && { createdBefore: endDate }),
};
const ordersResponse = await lovrabetClient.models.orders.filter(params);
const orders = ordersResponse.tableData;
// Calculate statistics
const stats = {
totalOrders: orders.length,
totalAmount: orders.reduce((sum, order) => sum + (order.amount || 0), 0),
averageAmount: 0,
statusBreakdown: {} as Record<string, number>,
dailyStats: {} as Record<string, { orders: number; amount: number }>,
};
stats.averageAmount =
stats.totalOrders > 0 ? stats.totalAmount / stats.totalOrders : 0;
// Status breakdown
orders.forEach((order) => {
stats.statusBreakdown[order.status] =
(stats.statusBreakdown[order.status] || 0) + 1;
// Group by date
const date = new Date(order.createdAt).toISOString().split("T")[0];
if (!stats.dailyStats[date]) {
stats.dailyStats[date] = { orders: 0, amount: 0 };
}
stats.dailyStats[date].orders += 1;
stats.dailyStats[date].amount += order.amount || 0;
});
res.json({
success: true,
data: stats,
});
} catch (error: any) {
console.error("Error fetching order analytics:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Scheduled Job Example
// src/jobs/dataSync.ts
import cron from "node-cron";
import { lovrabetClient } from "../api/client";
// Sync user data every hour
cron.schedule("0 * * * *", async () => {
console.log("Starting hourly user data sync...");
try {
// Get recently updated users
const response = await lovrabetClient.models.users.filter({
updatedAfter: new Date(Date.now() - 60 * 60 * 1000).toISOString(), // 1 hour ago
pageSize: 100,
});
console.log(`Found ${response.tableData.length} users to sync`);
// Process sync logic
for (const user of response.tableData) {
await syncUserToExternalSystem(user);
}
console.log("User data sync completed successfully");
} catch (error) {
console.error("Error during user data sync:", error);
}
});
// Generate daily report
cron.schedule("0 9 * * *", async () => {
console.log("Generating daily report...");
try {
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000);
const startOfDay = new Date(yesterday.setHours(0, 0, 0, 0));
const endOfDay = new Date(yesterday.setHours(23, 59, 59, 999));
// Get yesterday's orders
const ordersResponse = await lovrabetClient.models.orders.filter({
createdAfter: startOfDay.toISOString(),
createdBefore: endOfDay.toISOString(),
pageSize: 1000,
});
// Generate report
const report = {
date: yesterday.toISOString().split("T")[0],
totalOrders: ordersResponse.total,
totalAmount: ordersResponse.tableData.reduce(
(sum, order) => sum + order.amount,
0
),
statusBreakdown: {},
};
console.log("Daily report generated:", report);
// Send report to monitoring system or email
await sendDailyReport(report);
} catch (error) {
console.error("Error generating daily report:", error);
}
});
async function syncUserToExternalSystem(user: any) {
// Logic to sync to external system
console.log(`Syncing user ${user.id} to external system`);
}
async function sendDailyReport(report: any) {
// Logic to send report
console.log("Sending daily report:", report);
}
📱 Next.js Full-stack Application
API Routes
// pages/api/users/index.ts
import type { NextApiRequest, NextApiResponse } from "next";
import { lovrabetClient } from "../../../lib/lovrabet";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
switch (req.method) {
case "GET":
try {
const { page = 1, size = 20, ...filters } = req.query;
const response = await lovrabetClient.models.users.filter({
currentPage: parseInt(page as string),
pageSize: parseInt(size as string),
...filters,
});
res.status(200).json(response);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
break;
case "POST":
try {
const userData = req.body;
const newUser = await lovrabetClient.models.users.create(userData);
res.status(201).json(newUser);
} catch (error: any) {
res.status(500).json({ error: error.message });
}
break;
default:
res.setHeader("Allow", ["GET", "POST"]);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
Server-side Rendering
// pages/users/[id].tsx
import { GetServerSideProps } from "next";
import { lovrabetClient } from "../../lib/lovrabet";
import type { User } from "../../types/api";
interface UserDetailProps {
user: User;
orders: any[];
}
export default function UserDetail({ user, orders }: UserDetailProps) {
return (
<div>
<h1>User: {user.name}</h1>
<p>Email: {user.email}</p>
<p>Status: {user.status}</p>
<h2>Orders ({orders.length})</h2>
{orders.map((order) => (
<div key={order.id}>
Order #{order.id} - ${order.amount}
</div>
))}
</div>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const { id } = context.params!;
try {
// Fetch user info and orders in parallel
const [user, ordersResponse] = await Promise.all([
lovrabetClient.models.users.getOne(id as string),
lovrabetClient.models.orders.filter({
userId: id as string,
pageSize: 50,
}),
]);
return {
props: {
user,
orders: ordersResponse.tableData,
},
};
} catch (error) {
return {
notFound: true,
};
}
};
🔄 List Sorting Examples
Basic Sorting Usage
// Get user list sorted by creation time descending
const getRecentUsers = async () => {
const response = await lovrabetClient.models.users.filter(
{ currentPage: 1, pageSize: 20 },
[{ createTime: "desc" }]
);
return response.tableData;
};
// Get product list sorted by name ascending
const getProductsByName = async () => {
const response = await lovrabetClient.models.products.filter(
{ currentPage: 1, pageSize: 50 },
[{ name: "asc" }]
);
return response.tableData;
};
Multi-field Sorting
// Get orders: high priority first, then by creation time
const getPrioritizedOrders = async () => {
const response = await lovrabetClient.models.orders.filter(
{ currentPage: 1, pageSize: 100 },
[
{ priority: "desc" }, // Priority descending (high priority first)
{ createTime: "desc" }, // Creation time descending (newest first)
]
);
return response.tableData;
};
// Get student list: sorted by class and score
const getStudentsByClass = async (grade: number) => {
const response = await lovrabetClient.models.students.filter(
{
grade, // Filter by grade
currentPage: 1,
pageSize: 50,
},
[
{ className: "asc" }, // Class ascending
{ score: "desc" }, // Score descending
{ name: "asc" }, // Name ascending
]
);
return response.tableData;
};
Using Sorting in React Components
// components/SortableUserList.tsx
import { useState, useEffect } from "react";
import { lovrabetClient } from "../api/client";
import { SortOrder } from "@lovrabet/sdk";
interface User {
id: string;
name: string;
email: string;
createTime: string;
lastLoginTime: string;
}
export const SortableUserList = () => {
const [users, setUsers] = useState<User[]>([]);
const [sortField, setSortField] = useState<string>("createTime");
const [sortOrder, setSortOrder] = useState(SortOrder.DESC);
const [loading, setLoading] = useState(false);
const loadUsers = async () => {
setLoading(true);
try {
const response = await lovrabetClient.models.users.filter(
{ currentPage: 1, pageSize: 20 },
[{ [sortField]: sortOrder }]
);
setUsers(response.tableData);
} catch (error) {
console.error("Failed to load users:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadUsers();
}, [sortField, sortOrder]);
const handleSort = (field: string) => {
if (field === sortField) {
// Toggle sort direction
setSortOrder((prev) => (prev === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC));
} else {
// New field, default descending
setSortField(field);
setSortOrder(SortOrder.DESC);
}
};
return (
<div className="user-list">
<table>
<thead>
<tr>
<th onClick={() => handleSort("name")}>
Name {sortField === "name" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
<th onClick={() => handleSort("email")}>
Email {sortField === "email" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
<th onClick={() => handleSort("createTime")}>
Created Time{" "}
{sortField === "createTime" && (sortOrder === "asc" ? "↑" : "↓")}
</th>
<th onClick={() => handleSort("lastLoginTime")}>
Last Login{" "}
{sortField === "lastLoginTime" &&
(sortOrder === "asc" ? "↑" : "↓")}
</th>
</tr>
</thead>
<tbody>
{loading ? (
<tr>
<td colSpan={4}>Loading...</td>
</tr>
) : (
users.map((user) => (
<tr key={user.id}>
<td>{user.name}</td>
<td>{user.email}</td>
<td>{new Date(user.createTime).toLocaleString()}</td>
<td>{new Date(user.lastLoginTime).toLocaleString()}</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
};
Using Sorting in Vue Components
<!-- components/SortableProductList.vue -->
<template>
<div class="product-list">
<div class="sort-controls">
<label>
Sort Field:
<select v-model="sortField">
<option value="name">Product Name</option>
<option value="price">Price</option>
<option value="sales">Sales</option>
<option value="createTime">Created Time</option>
</select>
</label>
<label>
Sort Direction:
<select v-model="sortOrder">
<option :value="SortOrder.ASC">Ascending</option>
<option :value="SortOrder.DESC">Descending</option>
</select>
</label>
</div>
<div v-if="loading">Loading...</div>
<div v-else class="products">
<div v-for="product in products" :key="product.id" class="product-card">
<h3>{{ product.name }}</h3>
<p>Price: ¥{{ product.price }}</p>
<p>Sales: {{ product.sales }}</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
import { SortOrder, type SortList } from "@lovrabet/sdk";
import { lovrabetClient } from "@/api/client";
interface Product {
id: string;
name: string;
price: number;
sales: number;
createTime: string;
}
const products = ref<Product[]>([]);
const loading = ref(false);
const sortField = ref("sales");
const sortOrder = ref(SortOrder.DESC);
const loadProducts = async () => {
loading.value = true;
try {
const sortList: SortList = [{ [sortField.value]: sortOrder.value }];
const response = await lovrabetClient.models.products.filter(
{ currentPage: 1, pageSize: 50 },
sortList
);
products.value = response.tableData;
} catch (error) {
console.error("Failed to load products:", error);
} finally {
loading.value = false;
}
};
// Watch for sorting changes
watch([sortField, sortOrder], loadProducts, { immediate: true });
</script>
Using Sorting in Node.js API
// routes/api.ts
import express from "express";
import { SortOrder, type SortList } from "@lovrabet/sdk";
import { lovrabetClient } from "../config/lovrabet";
const router = express.Router();
// Get user list (with sorting support)
router.get("/users", async (req, res) => {
try {
const { page = 1, size = 20, sortBy, order } = req.query;
// Build sort list
let sortList: SortList | undefined;
if (sortBy && order) {
const sortOrder = order === "asc" ? SortOrder.ASC : SortOrder.DESC;
sortList = [{ [sortBy as string]: sortOrder }];
}
const response = await lovrabetClient.models.users.filter(
{
currentPage: parseInt(page as string),
pageSize: parseInt(size as string),
},
sortList
);
res.json({
success: true,
data: response.tableData,
total: response.total,
page: response.currentPage,
});
} catch (error) {
res.status(500).json({
success: false,
error: "Failed to fetch user list",
});
}
});
// Get trending products (complex sorting)
router.get("/products/trending", async (req, res) => {
try {
const response = await lovrabetClient.models.products.filter(
{
status: "active",
currentPage: 1,
pageSize: 20,
},
[
{ sales: SortOrder.DESC }, // Sales descending
{ rating: SortOrder.DESC }, // Rating descending
{ createTime: SortOrder.DESC }, // Creation time descending
]
);
res.json({
success: true,
data: response.tableData,
});
} catch (error) {
res.status(500).json({
success: false,
error: "Failed to fetch trending products",
});
}
});
export default router;
📥 Data Export Examples v1.1.24+
Using Excel Export in React
// src/components/UserExportButton.tsx
import React, { useState } from "react";
import { lovrabetClient } from "../api/client";
import type { ListParams } from "@lovrabet/sdk";
interface ExportButtonProps {
filters?: ListParams;
buttonText?: string;
}
export const UserExportButton: React.FC<ExportButtonProps> = ({
filters = {},
buttonText = "Export Excel",
}) => {
const [exporting, setExporting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleExport = async () => {
setExporting(true);
setError(null);
try {
// Call excelExport to export data
const fileUrl = await lovrabetClient.models.users.excelExport(filters);
// Open download link in new window
window.open(fileUrl, "_blank");
console.log("Export successful:", fileUrl);
} catch (err: any) {
const errorMessage = err?.message || "Export failed";
setError(errorMessage);
console.error("Export failed:", err);
} finally {
setExporting(false);
}
};
return (
<div className="export-container">
<button onClick={handleExport} disabled={exporting} className="export-btn">
{exporting ? "Exporting..." : buttonText}
</button>
{error && <div className="error-message">Error: {error}</div>}
</div>
);
};
// Usage example: Add export button to user list page
export const UserListWithExport: React.FC = () => {
const [filters, setFilters] = useState<ListParams>({
status: "active",
});
return (
<div className="user-list-page">
<div className="toolbar">
<h2>User List</h2>
{/* Export all active users */}
<UserExportButton filters={filters} buttonText="Export Active Users" />
{/* Export all data (no filters) */}
<UserExportButton buttonText="Export All Users" />
</div>
<UserList filters={filters} />
</div>
);
};
Using Excel Export in Vue 3
<!-- src/components/DataExportButton.vue -->
<template>
<div class="export-container">
<button
@click="handleExport"
:disabled="exporting"
class="export-btn"
>
{{ exporting ? 'Exporting...' : buttonText }}
</button>
<div v-if="error" class="error-message">
Error: {{ error }}
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { lovrabetClient } from "@/api/client";
import type { ListParams } from "@lovrabet/sdk";
interface Props {
modelName: string;
filters?: ListParams;
buttonText?: string;
}
const props = withDefaults(defineProps<Props>(), {
buttonText: "Export Excel",
filters: () => ({}),
});
const exporting = ref(false);
const error = ref<string | null>(null);
const handleExport = async () => {
exporting.value = true;
error.value = null;
try {
// Call excelExport to export data
const fileUrl = await lovrabetClient.models[props.modelName].excelExport(
props.filters
);
// Open download link in new window
window.open(fileUrl, "_blank");
console.log("Export successful:", fileUrl);
} catch (err: any) {
error.value = err?.message || "Export failed";
console.error("Export failed:", err);
} finally {
exporting.value = false;
}
};
</script>
<style scoped>
.export-btn {
padding: 8px 16px;
background-color: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.export-btn:disabled {
background-color: #d9d9d9;
cursor: not-allowed;
}
.error-message {
color: #ff4d4f;
margin-top: 8px;
font-size: 14px;
}
</style>
<!-- Using in page -->
<template>
<div class="user-list-page">
<div class="toolbar">
<h2>Order List</h2>
<!-- Export data with current filters -->
<DataExportButton
model-name="orders"
:filters="currentFilters"
button-text="Export Current Orders"
/>
<!-- Export all data -->
<DataExportButton
model-name="orders"
button-text="Export All Orders"
/>
</div>
<OrderList />
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
import DataExportButton from "@/components/DataExportButton.vue";
const currentFilters = ref({
status: "completed",
createTime: "2025-01-01",
});
</script>
Server-side Export in Node.js
// src/routes/export.ts
import express from "express";
import { lovrabetClient } from "../config/lovrabet";
const router = express.Router();
// Export user data
router.get("/api/export/users", async (req, res) => {
try {
const { status, role, startDate, endDate } = req.query;
// Build filter conditions
const filters: any = {};
if (status) filters.status = status;
if (role) filters.role = role;
if (startDate) filters.createdAfter = startDate;
if (endDate) filters.createdBefore = endDate;
// Export data
const fileUrl = await lovrabetClient.models.users.excelExport(filters);
// Return file URL
res.json({
success: true,
fileUrl,
message: "Export successful",
});
} catch (error: any) {
console.error("Export failed:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
// Batch export multiple datasets
router.post("/api/export/batch", async (req, res) => {
try {
const { exports } = req.body; // { exports: [{ model: 'users', filters: {...} }] }
const results = await Promise.all(
exports.map(async ({ model, filters }) => {
const fileUrl = await lovrabetClient.models[model].excelExport(filters);
return {
model,
fileUrl,
success: true,
};
})
);
res.json({
success: true,
results,
message: `Successfully exported ${results.length} files`,
});
} catch (error: any) {
console.error("Batch export failed:", error);
res.status(500).json({
success: false,
error: error.message,
});
}
});
export default router;
Auto-download Exported Files
// utils/downloadHelper.ts
/**
* Auto-download file (by creating temporary a tag)
*/
export async function downloadExportedFile(
fileUrl: string,
filename: string = "export.xlsx"
): Promise<void> {
// Method 1: Use window.open directly (recommended)
window.open(fileUrl, "_blank");
// Method 2: Use fetch + Blob (requires CORS handling)
// try {
// const response = await fetch(fileUrl);
// const blob = await response.blob();
//
// const link = document.createElement('a');
// link.href = URL.createObjectURL(blob);
// link.download = filename;
// link.click();
//
// // Release URL object
// URL.revokeObjectURL(link.href);
// } catch (error) {
// console.error('Download failed:', error);
// throw error;
// }
}
// React Hook: Use export functionality
import { useState, useCallback } from "react";
import { lovrabetClient } from "../api/client";
import { downloadExportedFile } from "../utils/downloadHelper";
export function useExport(modelName: string) {
const [exporting, setExporting] = useState(false);
const [error, setError] = useState<string | null>(null);
const exportData = useCallback(
async (filters = {}, filename?: string) => {
setExporting(true);
setError(null);
try {
const fileUrl = await lovrabetClient.models[modelName].excelExport(
filters
);
await downloadExportedFile(fileUrl, filename);
return fileUrl;
} catch (err: any) {
const errorMessage = err?.message || "Export failed";
setError(errorMessage);
throw err;
} finally {
setExporting(false);
}
},
[modelName]
);
return {
exporting,
error,
exportData,
};
}
// Using Hook
function UserListPage() {
const { exporting, error, exportData } = useExport("users");
const handleExport = async () => {
try {
await exportData(
{ status: "active" },
`users_${new Date().toISOString().split("T")[0]}.xlsx`
);
alert("Export successful!");
} catch (error) {
alert("Export failed, please try again");
}
};
return (
<button onClick={handleExport} disabled={exporting}>
{exporting ? "Exporting..." : "Export Users"}
</button>
);
}
📖 Best Practices Summary
1. Error Boundary Handling
// React error boundary
class ApiErrorBoundary extends React.Component {
state = { hasError: false, error: null };
static getDerivedStateFromError(error: any) {
return { hasError: true, error };
}
componentDidCatch(error: any, errorInfo: any) {
console.error("API Error Boundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
return (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => window.location.reload()}>Reload Page</button>
</div>
);
}
return this.props.children;
}
}
2. Data Validation
// Using zod for data validation
import { z } from "zod";
const UserSchema = z.object({
name: z.string().min(1, "Name is required"),
email: z.string().email("Invalid email"),
status: z.enum(["active", "inactive", "pending"]),
role: z.enum(["admin", "user", "moderator"]),
});
const validateAndCreateUser = async (userData: unknown) => {
try {
const validatedData = UserSchema.parse(userData);
return await lovrabetClient.models.users.create(validatedData);
} catch (error) {
if (error instanceof z.ZodError) {
throw new Error(
`Validation failed: ${error.errors.map((e) => e.message).join(", ")}`
);
}
throw error;
}
};
📖 Next Steps
After reviewing these practical examples, you can continue learning:
- 🔧 Troubleshooting - Common issues and debugging tips
We hope these examples help you successfully integrate the Lovrabet SDK into your projects!