@mindmakr/gs-websdk
Version:
Web SDK for Guru SaaS System - Complete JavaScript/TypeScript SDK for building applications with dynamic schema management
884 lines (759 loc) • 24.8 kB
Markdown
Real-world examples and integration patterns for the Guru SaaS Web SDK.
- [Basic Usage](
- [React Integration](
- [Vue.js Integration](
- [Advanced Patterns](
- [Error Handling](
- [Multi-Tenant Applications](
```typescript
import { GuruSaaS } from '@mindmakr/gs-websdk';
// Production configuration (with reverse proxy)
const client = new GuruSaaS({
baseUrl: 'https://your-api-domain.com'
});
// OR Development configuration (direct service access)
const devClient = new GuruSaaS({
authUrl: 'http://localhost:4000',
globalDataUrl: 'http://localhost:5010',
aiServiceUrl: 'http://localhost:4001',
notificationUrl: 'http://localhost:5020'
});
async function login() {
try {
const { user, tokens } = await client.login('user@example.com', 'password');
console.log(`Welcome ${user.name}!`);
return user;
} catch (error) {
console.error('Login failed:', error.message);
throw error;
}
}
```
```typescript
async function createCustomerForm() {
// Create a schema template
const template = await client.schema.createSchemaTemplate({
name: 'Customer Registration',
code: 'customer_registration',
category: 'customer_management',
schema_definition: {
type: 'object',
properties: {
firstName: {
type: 'string',
title: 'First Name',
minLength: 2
},
lastName: {
type: 'string',
title: 'Last Name',
minLength: 2
},
email: {
type: 'string',
format: 'email',
title: 'Email Address'
},
phone: {
type: 'string',
pattern: '^[+]?[1-9]\\d{1,14}$',
title: 'Phone Number'
}
},
required: ['firstName', 'lastName', 'email']
}
});
return template;
}
async function submitCustomerData(templateId: string, customerData: any) {
const instance = await client.schema.createSchemaInstance({
template_id: templateId,
data: customerData
});
return instance;
}
```
The SDK includes a comprehensive advanced search system with the following features:
- **Dynamic Field Discovery**: Automatically searches all string fields in schema definitions
- **Permission-Aware**: Only searches templates user has access to
- **Full-Text Search**: PostgreSQL full-text search with ranking and highlighting
- **Caching**: Redis-based result caching for improved performance (sub-100ms)
- **Analytics**: Search performance tracking and optimization
- **Nested Field Support**: Searches nested objects (e.g., "general.displayName")
```typescript
async function searchCustomers(searchTerm: string, userId: string) {
// Basic search across all accessible templates
const basicResults = await client.schema.getSchemaInstances({
search: searchTerm,
userId: userId, // REQUIRED: User ID for permission validation
page: 1,
limit: 20
});
console.log('Search results:', basicResults.data);
console.log('Search performance:', basicResults.search_summary?.search_performance_ms + 'ms');
console.log('Templates searched:', basicResults.search_summary?.total_templates_searched);
return basicResults;
}
async function advancedSearch(userId: string) {
// Advanced search with filters and metadata
const results = await client.schema.getSchemaInstances({
search: 'premium customer',
entity_type: 'customer',
template_ids: [1, 2, 3], // Search specific templates only
include_metadata: true,
relevance_threshold: 0.7, // Only high-relevance results (0-1 scale)
userId: userId, // REQUIRED: User ID for permission validation
page: 1,
limit: 50
});
// Access search metadata for each result
results.data.forEach(item => {
if (item.search_metadata) {
console.log(`Item: ${item.instance_data?.name || 'Unknown'}`);
console.log(` Matched fields: ${item.search_metadata.matched_fields.join(', ')}`);
console.log(` Relevance score: ${item.search_metadata.relevance_score.toFixed(3)}`);
console.log(` Template ID: ${item.search_metadata.template_id}`);
console.log(` Permission validated: ${item.search_metadata.permission_validated}`);
}
});
// Performance insights and search summary
if (results.search_summary) {
console.log('\nSearch Summary:');
console.log(` Templates searched: ${results.search_summary.total_templates_searched}`);
console.log(` Fields searched: ${results.search_summary.fields_searched.join(', ')}`);
console.log(` Search performance: ${results.search_summary.search_performance_ms}ms`);
console.log(` Permission check: ${results.search_summary.permission_check_ms}ms`);
console.log(` Accessible templates: ${results.search_summary.accessible_templates}`);
console.log(` Total searchable fields: ${results.search_summary.total_searchable_fields}`);
}
return results;
}
async function searchWithPagination(searchTerm: string, userId: string) {
let allResults = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const results = await client.schema.getSchemaInstances({
search: searchTerm,
userId: userId,
page: page,
limit: 100
});
allResults.push(...results.data);
hasMore = page < results.pagination.totalPages;
page++;
}
return allResults;
}
```
```typescript
// hooks/useAuth.ts
import { useState, useEffect } from 'react';
import { GuruSaaS, User } from '@mindmakr/gs-websdk';
// Production configuration
const client = new GuruSaaS({
baseUrl: process.env.REACT_APP_API_URL!
});
// OR Development configuration
const devClient = new GuruSaaS({
authUrl: process.env.REACT_APP_AUTH_URL || 'http://localhost:4000',
globalDataUrl: process.env.REACT_APP_GLOBAL_DATA_URL || 'http://localhost:5010',
aiServiceUrl: process.env.REACT_APP_AI_SERVICE_URL || 'http://localhost:4001',
notificationUrl: process.env.REACT_APP_NOTIFICATION_URL || 'http://localhost:5020'
});
export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check if user is already authenticated
if (client.isAuthenticated()) {
client.getUserInfo()
.then(setUser)
.catch(() => setUser(null))
.finally(() => setLoading(false));
} else {
setLoading(false);
}
}, []);
const login = async (email: string, password: string) => {
try {
const { user } = await client.login(email, password);
setUser(user);
return user;
} catch (error) {
throw error;
}
};
const logout = async () => {
await client.logout();
setUser(null);
};
return {
user,
loading,
login,
logout,
isAuthenticated: !!user
};
}
```
```typescript
// components/AdvancedSearch.tsx
import React, { useState, useEffect, useMemo } from 'react';
import { GuruSaaS, SchemaInstance } from '@mindmakr/gs-websdk';
import { useAuth } from '../hooks/useAuth';
interface SearchResult extends SchemaInstance {
template_name?: string;
template_code?: string;
search_metadata?: {
matched_fields: string[];
relevance_score: number;
template_id: number;
permission_validated: boolean;
};
}
export function AdvancedSearch() {
const { user } = useAuth();
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [searchSummary, setSearchSummary] = useState<any>(null);
const [filters, setFilters] = useState({
entity_type: '',
template_ids: [] as number[],
include_metadata: true,
relevance_threshold: 0.5
});
const client = new GuruSaaS({
baseUrl: process.env.REACT_APP_API_URL!
});
// Debounced search
const debouncedSearchTerm = useMemo(() => {
const timer = setTimeout(() => searchTerm, 300);
return () => clearTimeout(timer);
}, [searchTerm]);
useEffect(() => {
if (searchTerm && user) {
performSearch();
} else {
setResults([]);
setSearchSummary(null);
}
}, [debouncedSearchTerm, filters, user]);
const performSearch = async () => {
if (!user) return;
setLoading(true);
try {
const response = await client.schema.getSchemaInstances({
search: searchTerm,
userId: user.id,
entity_type: filters.entity_type || undefined,
template_ids: filters.template_ids.length > 0 ? filters.template_ids : undefined,
include_metadata: filters.include_metadata,
relevance_threshold: filters.relevance_threshold,
page: 1,
limit: 50
});
setResults(response.data);
setSearchSummary(response.search_summary);
} catch (error) {
console.error('Search failed:', error);
setResults([]);
setSearchSummary(null);
} finally {
setLoading(false);
}
};
const highlightMatches = (text: string, matchedFields: string[]) => {
// Simple highlighting logic
let highlighted = text;
matchedFields.forEach(field => {
const regex = new RegExp(`(${searchTerm})`, 'gi');
highlighted = highlighted.replace(regex, '<mark>$1</mark>');
});
return highlighted;
};
return (
<div className="advanced-search">
<div className="search-header">
<h2>Advanced Search</h2>
<div className="search-input">
<input
type="text"
placeholder="Search across all fields..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="search-field"
/>
</div>
<div className="search-filters">
<select
value={filters.entity_type}
onChange={(e) => setFilters({...filters, entity_type: e.target.value})}
>
<option value="">All Entity Types</option>
<option value="customer">Customer</option>
<option value="user">User</option>
<option value="product">Product</option>
</select>
<label>
<input
type="checkbox"
checked={filters.include_metadata}
onChange={(e) => setFilters({...filters, include_metadata: e.target.checked})}
/>
Include Metadata
</label>
<label>
Relevance Threshold:
<input
type="range"
min="0"
max="1"
step="0.1"
value={filters.relevance_threshold}
onChange={(e) => setFilters({...filters, relevance_threshold: parseFloat(e.target.value)})}
/>
{filters.relevance_threshold}
</label>
</div>
</div>
{searchSummary && (
<div className="search-summary">
<p>
Found {results.length} results in {searchSummary.search_performance_ms}ms
across {searchSummary.total_templates_searched} templates
</p>
<p>
Fields searched: {searchSummary.fields_searched.join(', ')}
</p>
</div>
)}
<div className="search-results">
{loading && <div className="loading">Searching...</div>}
{results.map((result) => (
<div key={result.id} className="result-item">
<div className="result-header">
<h3>{result.template_name}</h3>
<span className="entity-type">{result.entity_type}</span>
{result.search_metadata && (
<span className="relevance-score">
Score: {result.search_metadata.relevance_score.toFixed(2)}
</span>
)}
</div>
<div className="result-content">
{Object.entries(result.instance_data).map(([key, value]) => (
<div key={key} className="field">
<strong>{key}:</strong>
<span
dangerouslySetInnerHTML={{
__html: result.search_metadata?.matched_fields.includes(key)
? highlightMatches(String(value), [key])
: String(value)
}}
/>
</div>
))}
</div>
{result.search_metadata && (
<div className="search-metadata">
<small>
Matched fields: {result.search_metadata.matched_fields.join(', ')}
</small>
</div>
)}
</div>
))}
{!loading && results.length === 0 && searchTerm && (
<div className="no-results">
No results found for "{searchTerm}"
</div>
)}
</div>
</div>
);
}
```
```typescript
// components/DynamicForm.tsx
import React, { useState, useEffect } from 'react';
import { GuruSaaS, SchemaTemplate } from '@mindmakr/gs-websdk';
interface DynamicFormProps {
templateId: string;
onSubmit: (data: any) => void;
}
export function DynamicForm({ templateId, onSubmit }: DynamicFormProps) {
const [template, setTemplate] = useState<SchemaTemplate | null>(null);
const [formData, setFormData] = useState<any>({});
const [loading, setLoading] = useState(true);
const client = new GuruSaaS({
baseUrl: process.env.REACT_APP_API_URL!
});
useEffect(() => {
async function loadTemplate() {
try {
const template = await client.schema.getSchemaTemplate(templateId);
setTemplate(template);
} catch (error) {
console.error('Failed to load template:', error);
} finally {
setLoading(false);
}
}
loadTemplate();
}, [templateId]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const instance = await client.schema.createSchemaInstance({
template_id: templateId,
data: formData
});
onSubmit(instance);
} catch (error) {
console.error('Failed to submit form:', error);
}
};
if (loading) return <div>Loading...</div>;
if (!template) return <div>Template not found</div>;
return (
<form onSubmit={handleSubmit}>
<h2>{template.name}</h2>
{Object.entries(template.schema_definition.properties).map(([key, field]: [string, any]) => (
<div key={key} className="form-field">
<label htmlFor={key}>{field.title || key}</label>
<input
id={key}
type={field.format === 'email' ? 'email' : 'text'}
required={template.schema_definition.required?.includes(key)}
value={formData[key] || ''}
onChange={(e) => setFormData({ ...formData, [key]: e.target.value })}
/>
</div>
))}
<button type="submit">Submit</button>
</form>
);
}
```
```typescript
// components/UserManager.tsx
import React, { useState, useEffect } from 'react';
import { GuruSaaS, User, PaginatedResponse } from '@mindmakr/gs-websdk';
export function UserManager() {
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
const client = new GuruSaaS({
baseUrl: process.env.REACT_APP_API_URL!
});
useEffect(() => {
loadUsers();
}, [page]);
const loadUsers = async () => {
try {
const response: PaginatedResponse<User> = await client.auth.getUsers({
page,
limit: 10
});
setUsers(response.data);
} catch (error) {
console.error('Failed to load users:', error);
} finally {
setLoading(false);
}
};
const createUser = async (userData: Partial<User>) => {
try {
const newUser = await client.auth.createUser(userData);
setUsers([...users, newUser]);
} catch (error) {
console.error('Failed to create user:', error);
}
};
if (loading) return <div>Loading users...</div>;
return (
<div>
<h2>User Management</h2>
<div className="user-list">
{users.map(user => (
<div key={user.id} className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
<span className={`status ${user.is_active ? 'active' : 'inactive'}`}>
{user.is_active ? 'Active' : 'Inactive'}
</span>
</div>
))}
</div>
<div className="pagination">
<button
onClick={() => setPage(page - 1)}
disabled={page === 1}
>
Previous
</button>
<span>Page {page}</span>
<button onClick={() => setPage(page + 1)}>
Next
</button>
</div>
</div>
);
}
```
```typescript
// composables/useAuth.ts
import { ref, computed } from 'vue';
import { GuruSaaS, User } from '@mindmakr/gs-websdk';
const client = new GuruSaaS({
baseUrl: import.meta.env.VITE_API_URL
});
const user = ref<User | null>(null);
const loading = ref(false);
export function useAuth() {
const isAuthenticated = computed(() => !!user.value);
const login = async (email: string, password: string) => {
loading.value = true;
try {
const result = await client.login(email, password);
user.value = result.user;
return result;
} finally {
loading.value = false;
}
};
const logout = async () => {
await client.logout();
user.value = null;
};
const checkAuth = async () => {
if (client.isAuthenticated()) {
try {
user.value = await client.getUserInfo();
} catch {
user.value = null;
}
}
};
return {
user: readonly(user),
loading: readonly(loading),
isAuthenticated,
login,
logout,
checkAuth
};
}
```
```vue
<!-- components/SchemaManager.vue -->
<template>
<div class="schema-manager">
<h2>Schema Templates</h2>
<div v-if="loading" class="loading">
Loading templates...
</div>
<div v-else class="template-grid">
<div
v-for="template in templates"
:key="template.id"
class="template-card"
@click="selectTemplate(template)"
>
<h3>{{ template.name }}</h3>
<p>{{ template.description }}</p>
<span class="category">{{ template.category }}</span>
</div>
</div>
<button @click="createNewTemplate" class="create-btn">
Create New Template
</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { GuruSaaS, SchemaTemplate } from '@mindmakr/gs-websdk';
const client = new GuruSaaS({
baseUrl: import.meta.env.VITE_API_URL
});
const templates = ref<SchemaTemplate[]>([]);
const loading = ref(true);
const emit = defineEmits<{
templateSelected: [template: SchemaTemplate]
}>();
onMounted(async () => {
try {
const response = await client.schema.getSchemaTemplates();
templates.value = response.data;
} catch (error) {
console.error('Failed to load templates:', error);
} finally {
loading.value = false;
}
});
const selectTemplate = (template: SchemaTemplate) => {
emit('templateSelected', template);
};
const createNewTemplate = () => {
// Navigate to template creation page
// or emit event to parent component
};
</script>
```
```typescript
// Per-request configuration
const templates = await client.schema.getSchemaTemplates({}, {
timeout: 60000,
tenantId: 'specific-tenant',
headers: {
'X-Custom-Header': 'value'
}
});
```
```typescript
async function batchEnhanceFields() {
const enhancements = await client.ai.batchEnhanceFields([
{
fieldName: 'email',
fieldType: 'email',
businessContext: 'customer registration',
enhancementType: 'validation'
},
{
fieldName: 'phone',
fieldType: 'phone',
businessContext: 'customer contact',
enhancementType: 'full'
}
]);
return enhancements;
}
```
```typescript
// Listen for authentication events
client.on('auth:login', (user) => {
console.log('User logged in:', user);
});
client.on('auth:logout', () => {
console.log('User logged out');
});
client.on('error', (error) => {
console.error('SDK Error:', error);
});
```
```typescript
import {
SDKError,
AuthenticationError,
NetworkError,
ValidationError
} from '@mindmakr/gs-websdk';
async function handleApiCall() {
try {
const result = await client.auth.getUsers();
return result;
} catch (error) {
if (error instanceof AuthenticationError) {
// Redirect to login
window.location.href = '/login';
} else if (error instanceof NetworkError) {
// Show retry option
showRetryDialog();
} else if (error instanceof ValidationError) {
// Show field-specific errors
showValidationErrors(error.fieldErrors);
} else if (error instanceof SDKError) {
// Handle other SDK errors
showErrorMessage(error.message);
} else {
// Handle unexpected errors
console.error('Unexpected error:', error);
}
}
}
```
```typescript
// Set up global error handling
client.on('error', (error) => {
// Log error for monitoring
console.error('SDK Error:', error);
// Handle specific error types
if (error instanceof AuthenticationError) {
// Clear user session and redirect
localStorage.removeItem('user');
window.location.href = '/login';
} else if (error instanceof NetworkError) {
// Show network error notification
showNotification('Network error. Please check your connection.', 'error');
}
});
```
```typescript
// Configure default tenant
const client = new GuruSaaS({
baseUrl: 'https://api.yourdomain.com',
defaultTenant: 'tenant-1'
});
// Override tenant for specific requests
const templates = await client.schema.getSchemaTemplates({}, {
tenantId: 'tenant-2'
});
```
```typescript
class TenantManager {
private clients: Map<string, GuruSaaS> = new Map();
getClient(tenantId: string): GuruSaaS {
if (!this.clients.has(tenantId)) {
const client = new GuruSaaS({
baseUrl: 'https://api.yourdomain.com',
defaultTenant: tenantId
});
this.clients.set(tenantId, client);
}
return this.clients.get(tenantId)!;
}
async switchTenant(tenantId: string) {
const client = this.getClient(tenantId);
// Re-authenticate for new tenant if needed
if (!client.isAuthenticated()) {
// Redirect to login or use stored credentials
}
return client;
}
}
```
For more comprehensive examples, check out:
- [Complete Todo App Example](../examples/todo-app-complete.tsx)
- [Advanced Integration Examples](../examples/advanced-integration-examples.md)
- [API Reference](./API_REFERENCE.md) for detailed SDK method documentation
- [Backend API Reference](../../.context/api-reference.md) for backend API specifications
- [GitHub Issues](https://github.com/mindmakr/guru-saas/issues) - Bug reports and feature requests
- [GitHub Discussions](https://github.com/mindmakr/guru-saas/discussions) - Questions and community support