@objectstack/client
v2.0.5
Published
Official Client SDK for ObjectStack Protocol
Readme
@objectstack/client
The official TypeScript client for ObjectStack.
Features
- Auto-Discovery: Connects to your ObjectStack server and automatically configures API endpoints.
- Typed Metadata: Retrieve Object and View definitions with full type support.
- Metadata Caching: ETag-based conditional requests for efficient metadata caching.
- Unified Data Access: Simple CRUD operations for any object in your schema.
- Batch Operations: Efficient bulk create/update/delete with transaction support.
- View Storage: Save, load, and share custom UI view configurations.
- Standardized Errors: Machine-readable error codes with retry guidance.
- Full Protocol Coverage: Implements all 13 API namespaces defined in
@objectstack/spec - 95+ Methods: Complete implementation of discovery, metadata, data, auth, workflow, realtime, AI, and more.
🤖 AI Development Context
Role: Browser/Node Client SDK Usage:
- Use this in Frontend Apps (React, etc.) or external Node scripts.
- Interacts with
packages/runtimeover HTTP.
Key namespaces:
client.meta: Metadata operations.client.data: Data operations.
Installation
pnpm add @objectstack/clientUsage
import { ObjectStackClient } from '@objectstack/client';
// 1. Initialize
const client = new ObjectStackClient({
baseUrl: 'http://localhost:3004', // Your ObjectStack Server URL
token: 'optional-auth-token'
});
async function main() {
// 2. Connect (Fetches system capabilities)
await client.connect();
// 3. Metadata Access
const todoSchema = await client.meta.getObject('todo_task');
console.log('Fields:', todoSchema.fields);
// Save Metadata (New Feature)
await client.meta.saveItem('object', 'my_custom_object', {
label: 'My Object',
fields: { name: { type: 'text' } }
});
// 4. Advanced Query
const tasks = await client.data.find<{ subject: string; priority: number }>('todo_task', {
select: ['subject', 'priority'], // Field Selection
filters: ['priority', '>=', 2], // ObjectStack Filter AST
sort: ['-priority'], // Sorting
top: 10
});
// 5. Create Data
const newTask = await client.data.create('todo_task', {
subject: 'New Task',
priority: 1
});
// 6. Batch Operations (New!)
const batchResult = await client.data.batch('todo_task', {
operation: 'update',
records: [
{ id: '1', data: { status: 'active' } },
{ id: '2', data: { status: 'active' } }
],
options: {
atomic: true, // Rollback on any failure
returnRecords: true // Include full records in response
}
});
console.log(`Updated ${batchResult.succeeded} records`);
// 7. Metadata Caching (New!)
const cachedObject = await client.meta.getCached('todo_task', {
ifNoneMatch: '"686897696a7c876b7e"' // ETag from previous request
});
if (cachedObject.notModified) {
console.log('Using cached metadata');
}
// 8. View Storage (New!)
const view = await client.views.create({
name: 'active_tasks',
label: 'Active Tasks',
object: 'todo_task',
type: 'list',
visibility: 'public',
query: {
object: 'todo_task',
where: { status: 'active' },
orderBy: [{ field: 'priority', order: 'desc' }],
limit: 50
},
layout: {
columns: [
{ field: 'subject', label: 'Task', width: 200 },
{ field: 'priority', label: 'Priority', width: 100 }
]
}
});
}API Reference
client.connect()
Initializes the client by fetching the system discovery manifest from /api/v1.
client.meta
getObject(name: string): Get object schema.getCached(name: string, options?): Get object schema with ETag-based caching.getView(name: string): Get view configuration.
client.data
find<T>(object, options): Advanced query with filtering, sorting, selection.get<T>(object, id): Get single record by ID.query<T>(object, ast): Execute complex query using full AST.create<T>(object, data): Create record.batch(object, request): Recommended - Execute batch operations (create/update/upsert/delete) with full control.createMany<T>(object, data[]): Batch create records (convenience method).update<T>(object, id, data): Update record.updateMany<T>(object, records[], options?): Batch update records (convenience method).delete(object, id): Delete record.deleteMany(object, ids[], options?): Batch delete records (convenience method).
client.views (New!)
create(request): Create a new saved view.get(id): Get a saved view by ID.list(request?): List saved views with optional filters.update(request): Update an existing view.delete(id): Delete a saved view.share(id, userIds[]): Share a view with users/teams.setDefault(id, object): Set a view as default for an object.
Query Options
The find method accepts an options object:
select: Array of field names to retrieve.filters: Simple key-value map OR Filter AST['field', 'op', 'value'].sort: Sort string ('name') or array['-created_at', 'name'].top: Limit number of records.skip: Offset for pagination.
Batch Options
Batch operations support the following options:
atomic: If true, rollback entire batch on any failure (default: true).returnRecords: If true, return full record data in response (default: false).continueOnError: If true (and atomic=false), continue processing remaining records after errors.validateOnly: If true, validate records without persisting changes (dry-run mode).
Error Handling
The client provides standardized error handling with machine-readable error codes:
try {
await client.data.create('todo_task', { subject: '' });
} catch (error) {
console.error('Error code:', error.code); // e.g., 'validation_error'
console.error('Category:', error.category); // e.g., 'validation'
console.error('HTTP status:', error.httpStatus); // e.g., 400
console.error('Retryable:', error.retryable); // e.g., false
console.error('Details:', error.details); // Additional error info
}Error Code Reference
Client Errors (4xx)
| Error Code | HTTP Status | Retryable | Description |
|------------|-------------|-----------|-------------|
| validation_error | 400 | No | Input validation failed. Check error.details for field-specific errors |
| unauthenticated | 401 | No | Authentication required. Provide valid token |
| permission_denied | 403 | No | Insufficient permissions for this operation |
| resource_not_found | 404 | No | Resource does not exist. Verify object name or record ID |
| conflict | 409 | No | Resource conflict (e.g., duplicate unique field) |
| rate_limit_exceeded | 429 | Yes | Too many requests. Wait before retrying |
Server Errors (5xx)
| Error Code | HTTP Status | Retryable | Description |
|------------|-------------|-----------|-------------|
| internal_error | 500 | Yes | Server encountered an error. Retry with backoff |
| service_unavailable | 503 | Yes | Service temporarily unavailable. Retry later |
| gateway_timeout | 504 | Yes | Request timeout. Consider increasing timeout or retrying |
Retry Strategy Example:
async function retryableRequest<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error: any) {
if (!error.retryable || i === maxRetries - 1) {
throw error;
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
throw new Error('Max retries exceeded');
}
// Usage
const data = await retryableRequest(() =>
client.data.create('todo_task', { subject: 'Task' })
);Protocol Compliance
The @objectstack/client SDK implements all 13 API namespaces defined in the @objectstack/spec protocol specification:
| Namespace | Purpose | Status |
|-----------|---------|:------:|
| discovery | API version & capabilities detection | ✅ |
| meta | Metadata operations (objects, plugins, etc.) | ✅ |
| data | CRUD & query operations | ✅ |
| auth | Authentication & user management | ✅ |
| packages | Plugin/package lifecycle management | ✅ |
| views | UI view definitions | ✅ |
| workflow | Workflow state transitions | ✅ |
| analytics | Analytics queries | ✅ |
| automation | Automation triggers | ✅ |
| i18n | Internationalization | ✅ |
| notifications | Push notifications | ✅ |
| realtime | WebSocket subscriptions | ✅ |
| ai | AI services (NLQ, chat, insights) | ✅ |
For detailed compliance verification, see CLIENT_SPEC_COMPLIANCE.md.
Available Namespaces
Complete API Coverage
const client = new ObjectStackClient({ baseUrl: 'http://localhost:3000' });
await client.connect();
// Discovery & Metadata
await client.meta.getTypes(); // List metadata types
await client.meta.getItems('object'); // List all objects
await client.meta.getItem('object', 'contact'); // Get specific object
// Data Operations
await client.data.find('contact', { filters: { status: 'active' } });
await client.data.create('contact', { name: 'John' });
await client.data.update('contact', id, { status: 'inactive' });
await client.data.delete('contact', id);
await client.data.batch('contact', batchRequest);
// Authentication
await client.auth.login({ email: '[email protected]', password: 'pass' });
await client.auth.register({ email: '[email protected]', password: 'pass' });
await client.auth.me();
await client.auth.logout();
await client.auth.refreshToken('refresh-token-string');
// Package Management
await client.packages.list();
await client.packages.install({
name: 'vendor_plugin',
label: 'Vendor Plugin',
version: '1.0.0',
});
await client.packages.enable('plugin-id');
// Permissions
await client.permissions.check({ object: 'contact', action: 'create' });
await client.permissions.getObjectPermissions('contact');
await client.permissions.getEffectivePermissions();
// Workflow
await client.workflow.getConfig('approval');
await client.workflow.getState('approval', recordId);
await client.workflow.transition({ object: 'approval', recordId, transition: 'submit' });
await client.workflow.approve({ object: 'approval', recordId });
await client.workflow.reject({ object: 'approval', recordId, reason: 'Incomplete' });
// Realtime
await client.realtime.connect({ protocol: 'websocket' });
await client.realtime.subscribe({ channel: 'contact', event: 'update' });
await client.realtime.setPresence({ status: 'online' });
// Notifications
await client.notifications.registerDevice({ token: 'device-token', platform: 'ios' });
await client.notifications.list({ unreadOnly: true });
await client.notifications.markRead(['notif-1', 'notif-2']);
// AI Services
await client.ai.nlq({ query: 'Show me all active contacts' });
await client.ai.chat({ message: 'Summarize this project', context: projectId });
await client.ai.suggest({ object: 'contact', field: 'industry' });
await client.ai.insights({ object: 'sales', recordId: dealId });
// Internationalization
await client.i18n.getLocales();
await client.i18n.getTranslations('zh-CN');
await client.i18n.getFieldLabels('contact', 'zh-CN');
// Analytics
await client.analytics.query({ object: 'sales', aggregations: ['sum:amount'] });
await client.analytics.meta('sales');
// Automation
await client.automation.trigger('send_welcome_email', { userId });
// File Storage
await client.storage.upload(fileData, 'user');
await client.storage.getDownloadUrl('file-123');Testing
Unit Tests
pnpm testIntegration Tests
Note: Integration tests require a running ObjectStack server. The server is provided by a separate repository and must be set up independently.
# Start test server (in the ObjectStack server repository)
# Follow that project's documentation for test server setup
# Example: cd /path/to/objectstack-server && pnpm dev:test
# Run integration tests (in this repository)
cd packages/client
pnpm test:integrationSee CLIENT_SERVER_INTEGRATION_TESTS.md for detailed test specifications.
