@altanlabs/database v1.3.97
@altanlabs/database
A React library for database integration with simple hooks and Redux state management.
Installation
npm install @altanlabs/database
Quick Start
1. Set up the Provider
import { DatabaseProvider } from "@altanlabs/database";
const config = {
API_BASE_URL: "https://api.altan.ai/galaxia/hook/a9lcf",
SAMPLE_TABLES: {
todos: "550e8400-e29b-41d4-a716-446655440000" // Must be UUID
}
};
function App() {
return (
<DatabaseProvider config={config}>
<YourApp />
</DatabaseProvider>
);
}
2. Use the Hook
// Basic usage
function TodoList() {
const { records, isLoading, error } = useDatabase("todos");
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{/* Add Todo */}
<button onClick={() => addRecord({
text: "New Todo",
completed: false
})}>
Add Todo
</button>
{/* List Todos */}
{records.map(todo => (
<div key={todo.id}>
<span>{todo.text}</span>
<button onClick={() => modifyRecord(todo.id, {
completed: !todo.completed
})}>
Toggle
</button>
<button onClick={() => removeRecord(todo.id)}>
Delete
</button>
</div>
))}
</div>
);
}
// Advanced usage with initial query
function CompletedTodoList() {
const { records, isLoading, error } = useDatabase("todos", {
// Filter for completed todos
filters: [
{ field: "completed", operator: "eq", value: true }
],
// Sort by creation date
sort: [
{ field: "created_at", direction: "desc" }
],
// Limit to 10 records per page
limit: 10,
// Select specific fields
fields: ["id", "text", "completed", "created_at"],
// Get all records (alternative: "first" or "one")
amount: "all"
});
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{records.map(todo => (
<div key={todo.id}>
<span>{todo.text}</span>
<span>Created: {todo.created_at}</span>
</div>
))}
</div>
);
}
// Using multiple filters and complex queries
function FilteredProjectList() {
const { records } = useDatabase("projects", {
filters: [
{ field: "status", operator: "in", value: ["active", "pending"] },
{ field: "priority", operator: "gte", value: 2 },
{ field: "due_date", operator: "lte", value: new Date().toISOString() }
],
sort: [
{ field: "priority", direction: "desc" },
{ field: "due_date", direction: "asc" }
],
limit: 20,
fields: ["id", "title", "status", "priority", "due_date"]
});
// ... rest of the component
}
Important Notes
State Management
- The library automatically manages the Redux state. When you perform operations like
addRecord
,modifyRecord
, orremoveRecord
, the Redux store is automatically updated without requiring a manual refresh. - Only use
refresh()
when you explicitly need to re-fetch data from the server (e.g., when you want to sync with other users' changes).
Record IDs and Relationships
When working with records and relationships:
- Record IDs are integers
- Single relationships should be named with
_id
suffix:addRecord({ title: "Task", user_id: 3 // Single relationship })
- Multiple relationships should use the plural table name:
addRecord({ title: "Project", users: [1, 2, 3] // Multiple relationships })
Attachments
For fields of type attachment
, you can directly upload multiple media files. The field accepts an array of media objects:
// Upload new files
await addRecord({
title: "Document",
attachments: [{
file_name: "document.pdf",
mime_type: "application/pdf",
file_content: "base64_encoded_content..."
}]
});
// Update attachments - keep existing and add new
await modifyRecord(recordId, {
attachments: [
{ id: "existing_media_1", ... }, // Keep existing media
{ id: "existing_media_2", ... }, // Keep existing media
{ // Add new media
file_name: "new.pdf",
mime_type: "application/pdf",
file_content: "base64_encoded_content..."
}
]
});
// Remove all attachments
await modifyRecord(recordId, {
attachments: [] // Empty array removes all media
});
Behavior:
- Media objects with an
id
field are treated as references to existing media - Media objects without an
id
field are treated as new media to be created - Any existing media not included in the update will be deleted
- Sending an empty array (
[]
) ornull
will remove all media
Error Handling: The API will return a 400 Bad Request if:
- The media object format is invalid
- Required fields are missing
- The file content is not properly base64 encoded
- Referenced media IDs don't exist
Features
Query Options
// Refresh with new query
refresh({
// Filter records
filters: [
{ field: "status", operator: "eq", value: "active" }
],
// Sort records
sort: [
{ field: "created_time", direction: "desc" }
],
// Pagination
limit: 20,
pageToken: "next_page_token",
// Select specific fields
fields: ["id", "text", "completed"],
// Amount of records
amount: "all" // "all" | "first" | "one"
});
Pagination
function PaginatedList() {
const { records, nextPageToken, fetchNextPage } = useDatabase("todos");
return (
<div>
{records.map(todo => (
<div key={todo.id}>{todo.text}</div>
))}
{nextPageToken && (
<button onClick={fetchNextPage}>Load More</button>
)}
</div>
);
}
Bulk Operations
const { addRecords, removeRecords } = useDatabase("todos");
// Create multiple records
await addRecords([
{ text: "Todo 1", completed: false },
{ text: "Todo 2", completed: false }
]);
// Delete multiple records
await removeRecords(["id1", "id2", "id3"]);
Error Handling
const { addRecord } = useDatabase("todos");
try {
await addRecord(
{ text: "New Todo" },
(error) => console.error("Failed to add todo:", error)
);
} catch (error) {
console.error("Unexpected error:", error);
}
API Reference
useDatabase Hook Returns
{
// Data
records: TableRecordItem[] // Table records
schema: TableSchema | null // Table schema
// State
isLoading: boolean // Records loading state
schemaLoading: boolean // Schema loading state
error: string | null // Error message if any
nextPageToken: string | null // Pagination token
lastUpdated: string | null // Last update timestamp
// Methods - All methods accept an optional onError callback
refresh: (options?: FetchOptions) => Promise<void>
fetchNextPage: () => Promise<void>
// Record manipulation methods with return values
addRecord: (record: Record<string, unknown>) => Promise<TableRecordItem | undefined>
// Returns the newly created record or undefined if failed
modifyRecord: (id: string, updates: Record<string, unknown>) => Promise<TableRecordItem | undefined>
// Returns the updated record or undefined if failed
removeRecord: (id: string) => Promise<{ tableId: string; recordId: number } | undefined>
// Returns deletion confirmation or undefined if failed
addRecords: (records: Record<string, unknown>[]) => Promise<TableRecordItem[] | undefined>
// Returns array of newly created records or undefined if failed
removeRecords: (ids: string[]) => Promise<{ tableId: string; recordIds: number[] } | undefined>
// Returns deletion confirmation or undefined if failed
}
Working with Record Manipulation Returns
// Creating a record and using the return value
const newTodo = await addRecord({
text: "New Todo",
completed: false
});
if (newTodo) {
console.log('Created todo with ID:', newTodo.id);
}
// Creating multiple records
const newRecords = await addRecords([
{ text: "Todo 1", completed: false },
{ text: "Todo 2", completed: false }
]);
if (newRecords) {
console.log('Created todos with IDs:', newRecords.map(r => r.id));
}
// Updating a record
const updatedTodo = await modifyRecord(todoId, {
completed: true
});
if (updatedTodo) {
console.log('Todo updated:', updatedTodo);
}
// Deleting records with confirmation
const deleteResult = await removeRecords([id1, id2]);
if (deleteResult) {
console.log('Successfully deleted records:', deleteResult.recordIds);
}
License
MIT
⚠️ Important Performance Considerations
Avoiding Infinite Query Loops
The useDatabase
hook can potentially trigger infinite query loops if not used carefully. Here are some common pitfalls and how to avoid them:
❌ DON'T: Create multiple database hooks for the same data
// BAD: Each PlayerCard makes its own connection query
function PlayerCard({ userId, playerId }) {
const { records } = useDatabase("connections", {
filters: [
{ field: "user_id", operator: "eq", value: userId },
{ field: "player_id", operator: "eq", value: playerId }
]
});
// This creates N queries for N cards!
}
✅ DO: Lift database queries to the parent component
// GOOD: Parent component manages all connections
function PlayerList() {
const { records: connections } = useDatabase("connections", {
filters: [
{ field: "user_id", operator: "eq", value: userId }
],
enabled: !!userId // Prevent queries when userId is not available
});
return players.map(player => (
<PlayerCard
key={player.id}
player={player}
isConnected={connections.some(c => c.player_id === player.id)}
onConnect={handleConnect}
/>
));
}
Best Practices
Single Source of Truth:
- Keep database queries at the highest necessary level in your component tree
- Pass down data and handlers to child components as props
- Avoid querying the same table in multiple components
Control Query Execution:
// Use the enabled option to prevent unnecessary queries const { records } = useDatabase("table", { filters: [...], enabled: !!dependentData // Query only runs when dependentData exists });
Batch Operations:
- Use
addRecords
instead of multipleaddRecord
calls - Use
removeRecords
for bulk deletions
// GOOD: Single batch operation await addRecords(newItems); // BAD: Multiple individual operations for (const item of newItems) { await addRecord(item); // Don't do this! }
- Use
Optimize Queries:
- Only request fields you need using the
fields
option - Use appropriate
limit
values - Consider pagination for large datasets
- Only request fields you need using the
Common Anti-Patterns to Avoid
Nested Database Hooks:
// BAD: Nested queries can cause exponential number of requests function ParentComponent() { const { records: users } = useDatabase("users"); return users.map(user => <ChildWithDatabase userId={user.id} />); }
Unnecessary Refreshes:
// BAD: Don't refresh on every render or in short intervals useEffect(() => { refresh(); // This will cause continuous refreshes! });
Missing Error Handling:
// BAD: No error handling const handleClick = () => { addRecord(data); // Errors are silently ignored }; // GOOD: Proper error handling const handleClick = async () => { try { await addRecord(data, (error) => { console.error('Failed to add record:', error); notifyUser(error.message); }); } catch (error) { handleError(error); } };
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
7 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago
8 months ago