Official ObjectStack data adapter for Object UI.
This package provides the ObjectStackAdapter class, which connects Object UI's universal DataSource interface with the @objectstack/client SDK.
This enables strictly typed, metadata-driven UI components to communicate seamlessly with ObjectStack backends (Steedos, Salesforce, etc.).
npm install @object-ui/data-objectstackNote: @objectstack/client is a regular dependency of this package — it is installed and resolved along with it, so there is nothing to install separately.
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
import { SchemaRenderer } from '@object-ui/react';
// 1. Create the adapter
const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.example.com',
token: 'your-api-token' // Optional if effectively handling auth elsewhere
});
// 2. Pass to the Renderer
function App() {
return (
<SchemaRenderer
schema={mySchema}
dataSource={dataSource}
/>
);
}const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.example.com',
token: 'your-api-token',
// Configure metadata cache
cache: {
maxSize: 100, // Maximum number of cached schemas (default: 100)
ttl: 5 * 60 * 1000 // Time to live in ms (default: 5 minutes)
},
// Configure auto-reconnect
autoReconnect: true, // Enable auto-reconnect (default: true)
maxReconnectAttempts: 5, // Max reconnection attempts (default: 3)
reconnectDelay: 2000 // Initial delay between reconnects in ms (default: 1000)
});- ✅ CRUD Operations: Implements
find,findOne,create,update,delete. - ✅ Metadata Caching: Automatic LRU caching of schema metadata with TTL expiration.
- ✅ Metadata Fetching: Implements
getObjectSchemato power auto-generated forms and grids. - ✅ Query Translation: Converts Object UI's OData-like query parameters to ObjectStack's native query format.
- ✅ Bulk Operations: Supports optimized batch create/update/delete with detailed error reporting.
- ✅ Error Handling: Comprehensive error hierarchy with unique error codes and debugging details.
- ✅ Connection Monitoring: Real-time connection state tracking with event listeners.
- ✅ Auto-Reconnect: Automatic reconnection with exponential backoff on connection failures.
- ✅ Batch Progress: Progress events for tracking bulk operation status.
find() accepts Object UI's OData-style QueryParams and translates them into
ObjectStack's native query format, so a schema never has to be written in the
protocol's own shape:
// Query with filters (MongoDB-like operators)
const result = await dataSource.find('tasks', {
$filter: {
status: 'active',
priority: { $gte: 2 },
},
$orderby: { createdAt: 'desc' },
$top: 20,
$skip: 0,
});
// Escape hatch: reach the underlying ObjectStack client for anything
// the DataSource interface does not cover
const client = dataSource.getClient();
const metadata = await client.meta.getObject('task');Object UI ($) |
ObjectStack | Description |
|---|---|---|
$select |
select |
Field selection |
$filter |
filters (AST) |
Filter conditions (converted to FilterNode AST) |
$orderby |
sort |
Sort order |
$skip |
skip |
Pagination offset |
$top |
top |
Limit records |
The adapter converts MongoDB-like filter operators into ObjectStack FilterNode AST format. This is what keeps it compatible with the ObjectStack Protocol (v0.1.2+).
| MongoDB Operator | ObjectStack Operator | Example |
|---|---|---|
$eq or simple value |
= |
{ status: 'active' } → ['status', '=', 'active'] |
$ne |
!= |
{ status: { $ne: 'archived' } } → ['status', '!=', 'archived'] |
$gt |
> |
{ age: { $gt: 18 } } → ['age', '>', 18] |
$gte |
>= |
{ age: { $gte: 18 } } → ['age', '>=', 18] |
$lt |
< |
{ age: { $lt: 65 } } → ['age', '<', 65] |
$lte |
<= |
{ age: { $lte: 65 } } → ['age', '<=', 65] |
$in |
in |
{ status: { $in: ['active', 'pending'] } } → ['status', 'in', ['active', 'pending']] |
$nin / $notin |
notin |
{ status: { $nin: ['archived'] } } → ['status', 'notin', ['archived']] |
$contains / $regex |
contains |
{ name: { $contains: 'John' } } → ['name', 'contains', 'John'] |
$startswith |
startswith |
{ email: { $startswith: 'admin' } } → ['email', 'startswith', 'admin'] |
$between |
between |
{ age: { $between: [18, 65] } } → ['age', 'between', [18, 65]] |
Multiple conditions are combined with 'and':
// Input
const $filter = {
age: { $gte: 18, $lte: 65 },
status: 'active',
};
// Converted to AST
const ast = [
'and',
['age', '>=', 18],
['age', '<=', 65],
['status', '=', 'active'],
];// OData-style
await dataSource.find('users', {
$orderby: {
createdAt: 'desc',
name: 'asc',
},
});
// Converted to ObjectStack: ['-createdAt', 'name']The adapter includes built-in metadata caching to improve performance when fetching schemas:
// Get cache statistics
const stats = dataSource.getCacheStats();
console.log(`Cache hit rate: ${stats.hitRate * 100}%`);
console.log(`Cache size: ${stats.size}/${stats.maxSize}`);
// Manually invalidate cache entries
dataSource.invalidateCache('users'); // Invalidate specific schema
dataSource.invalidateCache(); // Invalidate all cached schemas
// Clear cache and statistics
dataSource.clearCache();- LRU Eviction: Automatically evicts least recently used entries when cache is full
- TTL Expiration: Entries expire after the configured time-to-live from creation (default: 5 minutes)
- Note: TTL is fixed from creation time, not sliding based on access
- Memory Limits: Configurable maximum cache size (default: 100 entries)
- Concurrent Access: Handles async operations safely. Note that concurrent requests for the same uncached key may result in multiple fetcher calls.
The adapter provides real-time connection state monitoring with automatic reconnection:
// Monitor connection state changes
const unsubscribe = dataSource.onConnectionStateChange((event) => {
console.log('Connection state:', event.state);
console.log('Timestamp:', new Date(event.timestamp));
if (event.error) {
console.error('Connection error:', event.error);
}
});
// Check current connection state
console.log(dataSource.getConnectionState()); // 'disconnected' | 'connecting' | 'connected' | 'reconnecting' | 'error'
// Check if connected
if (dataSource.isConnected()) {
console.log('Adapter is connected');
}
// Unsubscribe from events when done
unsubscribe();disconnected- Not connected to serverconnecting- Attempting initial connectionconnected- Successfully connectedreconnecting- Attempting to reconnect after failureerror- Connection failed (check event.error for details)
The adapter automatically attempts to reconnect on connection failures:
- Exponential Backoff: Delay increases with each attempt (delay × 2^(attempts-1))
- Configurable Attempts: Set
maxReconnectAttempts(default: 3) - Configurable Delay: Set
reconnectDelayfor initial delay (default: 1000ms) - Automatic: Enabled by default, disable with
autoReconnect: false
Track progress of bulk operations in real-time:
// Monitor batch operation progress
const unsubscribe = dataSource.onBatchProgress((event) => {
console.log(`${event.operation}: ${event.percentage.toFixed(1)}%`);
console.log(`Completed: ${event.completed}/${event.total}`);
console.log(`Failed: ${event.failed}`);
});
// Perform bulk operation
const users = await dataSource.bulk('users', 'create', largeDataset);
// Unsubscribe when done
unsubscribe();operation- Operation type ('create' | 'update' | 'delete')total- Total number of itemscompleted- Number of successfully completed itemsfailed- Number of failed itemspercentage- Completion percentage (0-100)
The adapter provides a comprehensive error hierarchy for better error handling:
import {
ObjectStackError, // Base error class
MetadataNotFoundError, // Schema/metadata not found (404)
BulkOperationError, // Bulk operation failures with partial results
ConnectionError, // Network/connection errors (503/504)
AuthenticationError, // Authentication failures (401/403)
DataApiValidationError, // Data validation errors (400). Its runtime `name` is
// still 'ValidationError' — that string is the wire
// discriminator shared with @objectstack/client. The
// SYMBOL is prefixed because @objectstack/spec/kernel
// owns `ValidationError` for a { field, message, code? }
// record (objectui#3160).
} from '@object-ui/data-objectstack';try {
const schema = await dataSource.getObjectSchema('users');
} catch (error) {
if (error instanceof MetadataNotFoundError) {
console.error(`Schema not found: ${error.details.objectName}`);
} else if (error instanceof ConnectionError) {
console.error(`Connection failed to: ${error.url}`);
} else if (error instanceof AuthenticationError) {
console.error('Authentication required');
}
// All errors have consistent structure
console.error({
code: error.code,
message: error.message,
statusCode: error.statusCode,
details: error.details
});
}Bulk operations provide detailed error reporting with partial success information:
try {
await dataSource.bulk('users', 'update', records);
} catch (error) {
if (error instanceof BulkOperationError) {
const summary = error.getSummary();
console.log(`${summary.successful} succeeded, ${summary.failed} failed`);
console.log(`Failure rate: ${summary.failureRate * 100}%`);
// Inspect individual failures
summary.errors.forEach(({ index, error }) => {
console.error(`Record ${index} failed:`, error);
});
}
}All errors include unique error codes for programmatic handling:
METADATA_NOT_FOUND- Schema/metadata not foundBULK_OPERATION_ERROR- Bulk operation failureCONNECTION_ERROR- Connection/network errorAUTHENTICATION_ERROR- Authentication failureVALIDATION_ERROR- Data validation errorUNSUPPORTED_OPERATION- Unsupported operationNOT_FOUND- Resource not foundUNKNOWN_ERROR- Unknown error
The adapter supports optimized batch operations with automatic fallback:
// Batch create
const newUsers = await dataSource.bulk('users', 'create', [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
]);
// Batch update (uses updateMany if available, falls back to individual updates)
const updated = await dataSource.bulk('users', 'update', [
{ id: '1', name: 'Alice Smith' },
{ id: '2', name: 'Bob Jones' },
]);
// Batch delete
await dataSource.bulk('users', 'delete', [
{ id: '1' },
{ id: '2' },
]);- Automatically uses
createMany,updateMany,deleteManywhen available - Falls back to individual operations with detailed error tracking
- Provides partial success reporting for resilient error handling
- Atomic operations where supported by the backend
bulk() above operates on one object. To persist a set of cross-object
writes as a single all-or-nothing unit — the master-detail case, where a parent
and its children must commit or roll back together — use batchTransaction:
// Create a parent and a child that references it, atomically.
// `{ $ref: 0 }` resolves to the id minted by operation 0 (the parent).
await dataSource.batchTransaction([
{ object: 'invoice', action: 'create', data: { no: 'INV-1' } },
{ object: 'invoice_line', action: 'create', data: { invoice: { $ref: 0 }, amount: 10 } },
]);On a supporting backend this is one POST /api/v1/batch that commits or rolls
back the whole set in a single server transaction — no orphaned parent if a
child write fails.
Whether the adapter can rely on server atomicity is decided at connect time,
not by firing a batch and reading the failure. connect() reads the
capabilities.transactionalBatch flag from GET /api/v1/discovery
(framework #3298),
which the server sets to true only when the /batch route is mounted and
the runtime engine can honour a transaction (declared === enforced):
Discovery capabilities.transactionalBatch |
batchTransaction behaviour |
|---|---|
true |
Trusts server atomicity. Calls /batch; any failure — including 404/405/501 — surfaces as a real error. No non-atomic client-side fallback. |
false |
Backend can't do an atomic batch (route absent, or a runtime without transactions) → falls back to the non-atomic client-side emulation below. |
| absent | Backend predates #3298 and advertises nothing → the legacy runtime probe stays: try /batch, and on 404/405/501 fall back to emulation. |
The hierarchical wire shape ({ transactionalBatch: { enabled: true } }) and the
flat form the client SDK normalizes to ({ transactionalBatch: true }) are both
accepted.
When the capability is false or absent, the adapter degrades to a client-side
emulation (@object-ui/core's emulateBatchTransaction): the operations run in
order and, on failure, it best-effort deletes the records it created (children
before parent) before rethrowing. This is not a transaction — a create's
side effects (hooks, rollups, webhooks) are not undone by a later delete, and a
mid-batch network drop leaves no chance to compensate. It exists only so a save
is still possible against a backend that lacks server atomicity; removing it
would turn "saves, less safe" into "no save path" on older backends
(objectui #2679).
Atomic cross-object saves are guaranteed only against ObjectStack backends on
the 16.x line that advertise capabilities.transactionalBatch: true — the
endpoint landed in framework #1604
and its discovery capability in
framework #3298.
ObjectUI does not hard-require it: against an older backend a master-detail save
still succeeds, but non-atomically via the fallback above. Treat the advertised
capability as the floor for the atomicity guarantee, not as a connection
prerequisite.
In addition to the main DataSource adapter, this package ships
createObjectStackUserStateAdapter — a small factory that lets Object UI
persist per-user UI state (favorites, recent items) into ObjectStack.
import { createObjectStackUserStateAdapter } from '@object-ui/data-objectstack';
import { useAttachUserStateAdapters } from '@object-ui/app-shell';
const favoritesAdapter = createObjectStackUserStateAdapter({
dataSource, // the ObjectStack DataSource
userId: user.id,
key: 'ui.favorites', // or 'ui.recent', 'ui.grid.account.state', ...
// resource: 'sys_user_preference', // default — the unified per-user KV store
// onError: (op, err) => console.warn(`[user-state] ${op} failed`, err),
});
attach('favorites', favoritesAdapter);The adapter writes to the platform's unified per-user KV store —
sys_user_preference — shipped by every @objectstack/plugin-auth
environment. One row per (user_id, key) pair:
object: sys_user_preference
fields:
- { name: user_id, type: lookup(sys_user), indexed: true }
- { name: key, type: string, indexed: true }
- { name: value, type: json }
- { name: updated_at, type: datetime }
unique: [user_id, key]By convention, namespace UI-trace keys under ui.* (e.g. ui.favorites,
ui.recent, ui.grid.<object>.state, ui.sidebar.collapsed) so they
stay easy to tell apart from user-facing preferences (theme, locale).
If the backend doesn't yet expose sys_user_preference, every call
404s and the UI silently degrades to localStorage-only persistence. See
User-Scoped State Persistence
for the full design.
new ObjectStackAdapter(config: {
baseUrl: string;
token?: string;
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
cache?: {
maxSize?: number;
ttl?: number;
};
autoReconnect?: boolean;
maxReconnectAttempts?: number;
reconnectDelay?: number;
})connect()- Establish connection to ObjectStack serverfind(resource, params?)- Query multiple recordsfindOne(resource, id, params?)- Get a single record by IDcreate(resource, data)- Create a new recordupdate(resource, id, data)- Update an existing recorddelete(resource, id)- Delete a recordbulk(resource, operation, data)- Batch operations (create/update/delete)batchTransaction(operations)- Cross-object atomic batch (master-detail); atomic when the backend advertisestransactionalBatch, else non-atomic client-side fallbackgetObjectSchema(objectName)- Get schema metadata (cached)getCacheStats()- Get cache statisticsinvalidateCache(key?)- Invalidate cache entriesclearCache()- Clear all cache entriesgetClient()- Access underlying ObjectStack clientgetConnectionState()- Get current connection stateisConnected()- Check if adapter is connectedonConnectionStateChange(listener)- Subscribe to connection state changes (returns unsubscribe function)onBatchProgress(listener)- Subscribe to batch operation progress (returns unsubscribe function)
- Enable Caching: Use default cache settings for optimal performance
- Handle Errors: Use typed error handling for better user experience
- Batch Operations: Use bulk methods for large datasets
- Monitor Cache: Check cache hit rates in production
- Invalidate Wisely: Clear cache after schema changes
- Connection Monitoring: Subscribe to connection state changes for better UX
- Auto-Reconnect: Use default auto-reconnect settings for resilient applications
- Batch Progress: Monitor progress for long-running bulk operations
// Error: MetadataNotFoundError
// Solution: Verify object name and ensure schema exists on server
const schema = await dataSource.getObjectSchema('correct_object_name');// Error: ConnectionError
// Solution: Check baseUrl and network connectivity
const dataSource = createObjectStackAdapter({
baseUrl: 'https://correct-url.example.com',
token: 'valid-token'
});// Clear cache if stale data is being returned
dataSource.clearCache();
// Or invalidate specific entries
dataSource.invalidateCache('users');MIT — see LICENSE.