@simpleapps-com/augur-api
Version:
TypeScript client library for Augur microservices API endpoints
887 lines (718 loc) โข 23.8 kB
Markdown
# Augur API Tips and Tricks
This document contains advanced usage patterns, performance optimizations, and helpful techniques for working with the Augur API client library.
## ๐ Finding Items by ItemId
### Problem: You have an itemId but need the invMastUid
When you only have a Prophet 21 itemId (like `WIRE-12-AWG-250FT`) but need the internal `invMastUid` for database operations:
```typescript
// โ
Use invMastUid = 0 with itemId as query pattern
const itemLookup = await api.items.invMast.doc.get(0, {
q: 'WIRE-12-AWG-250FT' // Your actual itemId
});
// Extract the invMastUid for future direct lookups
const invMastUid = itemLookup.data.invMastUid;
// Now use the invMastUid for efficient direct access
const itemDetails = await api.items.invMast.get(invMastUid);
const locations = await api.items.invMast.locations.list(invMastUid);
```
### Why this works
- The Items service recognizes `invMastUid = 0` as a special lookup case
- It searches the `item_id` field instead of using direct UID lookup
- Returns the complete document including the actual `invMastUid`
- This is the most efficient way to bridge itemId โ invMastUid
## ๐ Performance Optimization
### Smart Caching Strategies
```typescript
// โ
Cache stable reference data with longer TTL
const categories = await api.items.categories.list({
edgeCache: 8 // 8 hours - categories rarely change
});
const brands = await api.items.brands.list({
edgeCache: 12 // 12 hours - brand data is very stable
});
// โ
Shorter cache for semi-dynamic data
const inventory = await api.items.invMast.get(invMastUid, {
edgeCache: 2 // 2 hours - inventory levels change regularly
});
// โ
Very short cache for user-specific data
const userCart = await api.commerce.cartHeaders.list({
userId: 123,
edgeCache: 0.5 // 30 minutes - cart changes frequently
});
// โ Don't cache authentication or real-time operations
const authCheck = await api.joomla.users.verifyPassword({
username: 'user',
password: 'pass'
// No edgeCache - authentication must be real-time
});
```
### Batch Operations for Better Performance
```typescript
// โ
Get multiple items efficiently
const itemPromises = invMastUids.map(uid =>
api.items.invMast.get(uid, { edgeCache: 2 })
);
const items = await Promise.all(itemPromises);
// โ
Use parallel requests for independent data
const [categories, brands, attributes] = await Promise.all([
api.items.categories.list({ edgeCache: 8 }),
api.items.brands.list({ edgeCache: 8 }),
api.items.attributes.list({ edgeCache: 6 })
]);
```
## ๐ Cross-Service Data Patterns
### Order โ Item Details Workflow
```typescript
// Get order details
const order = await api.orders.oeHdr.get(orderNo);
const orderLines = await api.orders.oeHdr.lines.list(orderNo);
// Extract item IDs from order lines and get detailed item info
const itemDetailsPromises = orderLines.data.map(async (line) => {
// Use the itemId to invMastUid lookup pattern
const itemLookup = await api.items.invMast.doc.get(0, {
q: line.itemId
});
return {
orderLine: line,
itemDetails: await api.items.invMast.get(itemLookup.data.invMastUid),
locations: await api.items.invMast.locations.list(itemLookup.data.invMastUid)
};
});
const enrichedOrderLines = await Promise.all(itemDetailsPromises);
```
### Customer โ Purchased Items โ Recommendations
```typescript
// Get customer's purchase history
const customerId = 12345;
const purchasedItems = await api.customers.customer.purchasedItems.list(customerId, {
limit: 50,
edgeCache: 1 // Recent purchases change frequently
});
// Find similar items for recommendations using OpenSearch
const recommendationPromises = purchasedItems.data.slice(0, 5).map(async (item) => {
return api.openSearch.itemSearch.search({
q: item.itemId,
searchType: 'similarity',
size: 3,
edgeCache: 4 // Similarity results are relatively stable
});
});
const recommendations = await Promise.all(recommendationPromises);
```
## ๐ Error Handling Patterns
### Graceful Degradation
```typescript
async function getItemWithFallback(itemIdentifier: string | number) {
try {
// Try direct invMastUid lookup first
if (typeof itemIdentifier === 'number') {
return await api.items.invMast.get(itemIdentifier);
}
// Fallback to itemId lookup
const itemLookup = await api.items.invMast.doc.get(0, {
q: itemIdentifier
});
return await api.items.invMast.get(itemLookup.data.invMastUid);
} catch (error) {
// Final fallback to search
console.warn(`Direct lookup failed for ${itemIdentifier}, trying search...`);
const searchResults = await api.openSearch.itemSearch.search({
q: itemIdentifier.toString(),
searchType: 'query',
size: 1
});
if (searchResults.data.length === 0) {
throw new Error(`Item not found: ${itemIdentifier}`);
}
return searchResults.data[0];
}
}
```
### Request Timeout Handling
```typescript
import { AbortController } from 'node-abort-controller';
async function getItemWithTimeout(invMastUid: number, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const result = await api.items.invMast.get(invMastUid, {}, {
signal: controller.signal
});
clearTimeout(timeoutId);
return result;
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
throw error;
}
}
```
## ๐ Authentication Patterns
### Cross-Site Authentication
```typescript
// Set up cross-site authentication for multi-tenant scenarios
const api = createAugurApiClient({
baseUrl: 'https://api.example.com',
authToken: 'your-jwt-token',
siteId: 'site-specific-id' // For cross-site operations
});
// Use with services that support cross-site operations
const siteSpecificUsers = await api.joomla.users.list({
limit: 50
// siteId is automatically included in headers
});
```
### Token Refresh Pattern
```typescript
class ApiManager {
private client: AugurApiClient;
private refreshTokenIfNeeded: () => Promise<string>;
constructor(refreshCallback: () => Promise<string>) {
this.refreshTokenIfNeeded = refreshCallback;
this.client = createAugurApiClient({
baseUrl: 'https://api.example.com'
});
}
async makeAuthenticatedRequest<T>(
request: (client: AugurApiClient) => Promise<T>
): Promise<T> {
try {
return await request(this.client);
} catch (error) {
if (error.response?.status === 401) {
// Token expired, refresh and retry
const newToken = await this.refreshTokenIfNeeded();
this.client.updateAuthToken(newToken);
return await request(this.client);
}
throw error;
}
}
}
```
## ๐ Data Transformation Helpers
### Prophet 21 to Application Models
```typescript
// Transform P21 data structures to application-friendly formats
function transformInventoryItem(p21Item: any) {
return {
id: p21Item.invMastUid,
itemId: p21Item.itemId,
description: p21Item.itemDesc,
price: p21Item.listPrice,
availability: {
inStock: p21Item.qtyOnHand > 0,
quantity: p21Item.qtyOnHand,
locations: p21Item.locations?.map(loc => ({
warehouse: loc.locationId,
bin: loc.bin,
quantity: loc.qtyOnHand
})) || []
},
category: {
id: p21Item.itemCategoryUid,
name: p21Item.categoryName
}
};
}
// Use with API responses
const rawItem = await api.items.invMast.get(invMastUid);
const transformedItem = transformInventoryItem(rawItem.data);
```
## ๐งช Testing Patterns
### Mock API Responses
```typescript
// Create mock client for testing
const mockApi = {
items: {
invMast: {
get: jest.fn().mockResolvedValue({
status: 200,
data: { invMastUid: 123, itemId: 'TEST-ITEM' }
}),
doc: {
get: jest.fn().mockResolvedValue({
status: 200,
data: { invMastUid: 123, itemId: 'TEST-ITEM' }
})
}
}
}
};
// Test the itemId lookup pattern
test('should find item by itemId', async () => {
const result = await getItemByItemId('TEST-ITEM');
expect(mockApi.items.invMast.doc.get).toHaveBeenCalledWith(0, {
q: 'TEST-ITEM'
});
});
```
## ๐ฏ Common Use Cases
### E-commerce Product Catalog
```typescript
// Build complete product catalog with all necessary data
async function buildProductCatalog(categoryIds: number[]) {
const [categories, brands, attributes] = await Promise.all([
api.items.categories.list({ edgeCache: 8 }),
api.items.brands.list({ edgeCache: 8 }),
api.items.attributes.list({ edgeCache: 6 })
]);
const products = [];
for (const categoryId of categoryIds) {
const categoryItems = await api.openSearch.itemSearch.search({
itemCategoryUidList: categoryId.toString(),
searchType: 'query',
size: 100,
edgeCache: 4
});
const enrichedItems = await Promise.all(
categoryItems.data.map(async (item) => ({
...item,
locations: await api.items.invMast.locations.list(item.invMastUid, {
edgeCache: 2
}),
alternates: await api.items.invMast.alternateCode.list(item.invMastUid, {
edgeCache: 4
})
}))
);
products.push(...enrichedItems);
}
return { products, categories: categories.data, brands: brands.data };
}
```
### Inventory Management Dashboard
```typescript
// Real-time inventory status across multiple locations
async function getInventoryDashboard(itemIds: string[]) {
const inventoryData = await Promise.all(
itemIds.map(async (itemId) => {
// Use the special lookup pattern
const itemLookup = await api.items.invMast.doc.get(0, { q: itemId });
const invMastUid = itemLookup.data.invMastUid;
const [details, locations, movements] = await Promise.all([
api.items.invMast.get(invMastUid, { edgeCache: 1 }),
api.items.invMast.locations.list(invMastUid, { edgeCache: 0.5 }),
// Get recent activity (if available in your setup)
api.legacy.inventory.activity?.(itemId) || Promise.resolve({ data: [] })
]);
return {
itemId,
invMastUid,
details: details.data,
locations: locations.data,
totalQuantity: locations.data.reduce((sum, loc) => sum + loc.qtyOnHand, 0),
recentActivity: movements.data
};
})
);
return inventoryData;
}
```
## ๐ Advanced Request Patterns
### Request Cancellation
```typescript
const controller = new AbortController();
// Start a request
const usersPromise = api.joomla.users.list(
{ limit: 100 },
{ signal: controller.signal } // Pass abort signal
);
// Cancel the request if needed
setTimeout(() => {
controller.abort();
}, 5000);
try {
const users = await usersPromise;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request was cancelled');
}
}
```
### Custom Headers and Timeouts
```typescript
// Per-request configuration
const users = await api.joomla.users.list(
{ limit: 20 },
{
timeout: 10000, // 10 second timeout
headers: {
'X-Custom-Header': 'value',
'X-Request-ID': 'unique-id'
}
}
);
```
### Middleware Integration
```typescript
// Add global request/response interceptors
const api = new AugurAPI({
siteId: 'your-site-id',
bearerToken: 'your-token',
// Request interceptor
onRequest: (config) => {
console.log(`Making request to: ${config.url}`);
config.headers['X-Request-Timestamp'] = Date.now().toString();
return config;
},
// Response interceptor
onResponse: (response) => {
console.log(`Response status: ${response.status}`);
return response;
},
// Error interceptor
onError: (error) => {
console.error('API Error:', error.message);
// Custom error handling logic
throw error;
}
});
```
## ๐ Framework Integration Patterns
### React Hook Pattern
```tsx
import React, { useState, useEffect } from 'react';
import { AugurAPI, User, AugurAPIError, type AugurContext } from '@simpleapps-com/augur-api';
// Custom hook for Augur API - Context-aware approach
function useAugurAPI(context?: AugurContext) {
const [api] = useState(() => {
if (context) {
// Use context-aware creation (recommended)
return AugurAPI.fromContext(context);
}
// Fallback to manual configuration
return new AugurAPI({
siteId: process.env.REACT_APP_AUGUR_SITE_ID!,
bearerToken: getStoredToken()
});
});
return api;
}
// Component using the API
const UserList: React.FC = () => {
const api = useAugurAPI();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function fetchUsers() {
try {
setLoading(true);
setError(null);
const response = await api.joomla.users.list({
limit: 50,
edgeCache: 2 // Cache for 2 hours
});
setUsers(response.data);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
fetchUsers();
}, [api]);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.username}</li>
))}
</ul>
);
};
```
### Next.js API Route Integration
```typescript
// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { AugurAPI } from '@simpleapps-com/augur-api';
const api = new AugurAPI({
siteId: process.env.AUGUR_SITE_ID!,
bearerToken: process.env.AUGUR_TOKEN!
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const { limit = 10, offset = 0 } = req.query;
const users = await api.joomla.users.list({
limit: parseInt(limit as string),
offset: parseInt(offset as string),
edgeCache: 4 // 4 hours for server-side caching
});
res.status(200).json(users);
} catch (error) {
console.error('API Error:', error);
res.status(500).json({ error: 'Failed to fetch users' });
}
}
```
### React Native Pattern
```typescript
// useAugurAPI.ts - React Native hook
import { useState, useEffect } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { AugurAPI } from '@simpleapps-com/augur-api';
export function useAugurAPI() {
const [api, setApi] = useState<AugurAPI | null>(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
async function initializeAPI() {
try {
const [siteId, token] = await Promise.all([
AsyncStorage.getItem('augur_site_id'),
AsyncStorage.getItem('augur_token')
]);
if (siteId && token) {
const apiInstance = new AugurAPI({
siteId,
bearerToken: token
});
setApi(apiInstance);
}
} catch (error) {
console.error('Failed to initialize API:', error);
} finally {
setIsReady(true);
}
}
initializeAPI();
}, []);
return { api, isReady };
}
```
## ๐ก๏ธ Advanced Error Handling
### Comprehensive Error Handling Pattern
```typescript
import {
AugurAPIError,
AuthenticationError,
ValidationError,
NotFoundError,
RateLimitError,
NetworkError
} from '@simpleapps-com/augur-api';
async function handleAPICall() {
try {
const result = await api.joomla.users.list({ limit: 10 });
return result;
} catch (error) {
// Handle specific error types
if (error instanceof AuthenticationError) {
console.error('Authentication failed:', error.message);
switch (error.statusCode) {
case 401:
await refreshToken();
break;
default:
redirectToLogin();
}
} else if (error instanceof ValidationError) {
console.error('Request validation failed:', error.validationErrors);
error.validationErrors?.forEach(validationError => {
console.error(`Field ${validationError.path}: ${validationError.message}`);
});
} else if (error instanceof NotFoundError) {
console.error('Resource not found:', error.message);
showNotFoundMessage(error.endpoint);
} else if (error instanceof RateLimitError) {
console.error('Rate limit exceeded:', error.message);
// Implement exponential backoff
await new Promise(resolve => setTimeout(resolve, 2000));
return handleAPICall(); // Retry
} else if (error instanceof AugurAPIError) {
console.error('API error:', {
code: error.code,
message: error.message,
service: error.service,
endpoint: error.endpoint,
statusCode: error.statusCode,
requestId: error.requestId
});
switch (error.statusCode) {
case 500:
showErrorMessage('Server error. Please try again later.');
break;
case 503:
showErrorMessage('Service temporarily unavailable.');
break;
default:
showErrorMessage(`API error: ${error.message}`);
}
}
throw error; // Re-throw if needed
}
}
```
### Retry Logic with Exponential Backoff
```typescript
async function apiCallWithRetry<T>(
operation: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000
): Promise<T> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
// Don't retry on authentication or validation errors
if (error instanceof AuthenticationError ||
error instanceof ValidationError) {
throw error;
}
// Don't retry on final attempt
if (attempt === maxRetries) {
throw error;
}
// Calculate delay with exponential backoff
const delay = baseDelay * Math.pow(2, attempt - 1);
console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw new Error('All retry attempts exhausted');
}
// Usage
const users = await apiCallWithRetry(() =>
api.joomla.users.list({ limit: 10 })
);
```
### Global Error Handler
```typescript
class ErrorHandler {
static handle(error: unknown, context?: string): void {
const contextPrefix = context ? `[${context}] ` : '';
if (error instanceof AuthenticationError) {
console.error(`${contextPrefix}Authentication failed:`, error.message);
this.handleAuthError(error);
} else if (error instanceof ValidationError) {
console.error(`${contextPrefix}Validation failed:`, error.validationErrors);
this.handleValidationError(error);
} else if (error instanceof AugurAPIError) {
console.error(`${contextPrefix}API error:`, {
service: error.service,
endpoint: error.endpoint,
status: error.statusCode,
message: error.message
});
this.handleAPIError(error);
} else {
console.error(`${contextPrefix}Unexpected error:`, error);
}
}
private static handleAuthError(error: AuthenticationError) {
// Implement token refresh logic
if (typeof window !== 'undefined') {
window.location.href = '/login';
}
}
private static handleValidationError(error: ValidationError) {
// Show user-friendly validation messages
if (error.validationErrors) {
error.validationErrors.forEach(err => {
showFieldError(err.path, err.message);
});
}
}
private static handleAPIError(error: AugurAPIError) {
// Log to monitoring service
if (typeof window !== 'undefined' && window.analytics) {
window.analytics.track('API Error', {
service: error.service,
endpoint: error.endpoint,
statusCode: error.statusCode
});
}
}
}
```
## ๐๏ธ Advanced Caching Strategies
### Edge Cache Duration Guide
| Value | Duration | Best For | Example Use Cases |
|-------|----------|----------|-------------------|
| `1` | 1 hour | Frequently changing data | Cart contents, user sessions, real-time pricing |
| `2` | 2 hours | Moderate volatility | User lists, inventory levels, order status |
| `3` | 3 hours | Standard business data | Standard pricing, product information, customer data |
| `4` | 4 hours | Semi-static data | Recommendations, warehouse data, shipping rates |
| `5` | 5 hours | Reference data | Tags, categories, attribute lists |
| `8` | 8 hours | Static content | Brand information, distributor details, system configs |
### Smart Cache Implementation
```typescript
// Cache strategy based on data volatility
class CacheStrategy {
static getOptimalTTL(dataType: string, context?: any): number {
switch (dataType) {
case 'user_session':
case 'cart_contents':
case 'real_time_pricing':
return 1; // 1 hour
case 'inventory_levels':
case 'order_status':
case 'user_profiles':
return 2; // 2 hours
case 'product_catalog':
case 'standard_pricing':
case 'customer_data':
return 3; // 3 hours
case 'recommendations':
case 'warehouse_locations':
case 'shipping_rates':
return 4; // 4 hours
case 'categories':
case 'attributes':
case 'tags':
return 5; // 5 hours
case 'brands':
case 'system_config':
case 'distributor_info':
return 8; // 8 hours
default:
return 2; // Safe default
}
}
}
// Usage in API calls
const categories = await api.items.categories.list({
edgeCache: CacheStrategy.getOptimalTTL('categories')
});
const userCart = await api.commerce.cartHeaders.list({
userId: 123,
edgeCache: CacheStrategy.getOptimalTTL('cart_contents')
});
```
### Cache Invalidation Patterns
```typescript
// Cache warming for critical data
async function warmCache() {
const criticalData = [
() => api.items.categories.list({ edgeCache: 8 }),
() => api.items.brands.list({ edgeCache: 8 }),
() => api.items.attributes.list({ edgeCache: 5 }),
];
// Warm cache in parallel
await Promise.all(criticalData.map(fn => fn()));
}
// Cache busting for updated data
async function updateProductAndInvalidateCache(productId: number, updates: any) {
// Update the product
await api.items.invMast.update(productId, updates);
// Force refresh cached data (edgeCache: 0 bypasses cache)
const freshData = await api.items.invMast.get(productId, { edgeCache: 0 });
return freshData;
}
```
---
## ๐ก Pro Tips
1. **Always cache reference data** - Categories, brands, and attributes change infrequently
2. **Use the itemId โ invMastUid lookup** - It's the bridge between user-facing IDs and internal UIDs
3. **Batch related requests** - Use `Promise.all()` for independent parallel requests
4. **Handle 404s gracefully** - Not all items exist in all systems
5. **Monitor cache hit rates** - Adjust TTL values based on actual usage patterns
6. **Use TypeScript** - The client provides full type safety for better development experience
---
*This document is part of the [Augur API Client Library](./README.md) package and is included in npm distributions for offline reference.*