ObjectStack Data Adapter
Data adapter for connecting ObjectUI to ObjectStack backends
ObjectStack Data Adapter
The @object-ui/data-objectstack package provides a data adapter that connects ObjectUI to ObjectStack backends. It enables seamless integration with ObjectStack's data layer, supporting CRUD operations, queries, and real-time updates.
Installation
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.
Overview
The ObjectStack adapter bridges ObjectUI's schema-driven UI with ObjectStack's data backend, providing:
- š Automatic Integration - Connect to ObjectStack APIs
- š CRUD Operations - Create, read, update, delete
- š Query Builder - Build complex queries
- š Data Binding - Automatic data synchronization
- š Real-time Updates - Live data updates (if supported by backend)
Features
- ObjectStack Client Integration - Uses
@objectstack/clientfor API communication - Headless - No React dependency; the adapter is a plain
DataSourceobject you inject at the renderer boundary - Query Support - Full ObjectQL query support
- Automatic Field Mapping - Maps ObjectStack schemas to UI components
- Type Safety - Full TypeScript support
- Error Handling - Comprehensive error handling and retry logic
Quick Start
1. Create the adapter
This package is headless ā it exports no React components and no hooks. You create a plain adapter object:
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.example.com',
token: 'your-api-token', // optional if auth is handled elsewhere
});createObjectStackAdapter returns an ObjectStackAdapter ā the concrete adapter
class, which implements DataSource, the universal interface every ObjectUI
renderer consumes. new ObjectStackAdapter(config) is the class form of the same
thing and has the same type. Annotate the value as DataSource wherever you want
only the universal surface.
2. Inject it at the renderer boundary
React wiring lives in @object-ui/react, not here. Wrap your tree in
SchemaRendererProvider and hand it the adapter:
import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
import type { ObjectGridSchema } from '@object-ui/types';
const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.example.com',
});
const mySchema: ObjectGridSchema = { type: 'object-grid', objectName: 'user' };
function App() {
return (
<SchemaRendererProvider dataSource={dataSource}>
<SchemaRenderer schema={mySchema} />
</SchemaRendererProvider>
);
}SchemaRenderer also accepts an explicit dataSource prop
(<SchemaRenderer schema={mySchema} dataSource={dataSource} />) when you would
rather inject per render than through context.
3. Bind a block to data
A schema node's dataSource key is metadata, not the adapter ā it is the
spec's per-element binding describing what to query, while the adapter above
describes how to reach the backend:
{
"type": "list-view",
"dataSource": { "object": "user", "view": "active", "limit": 50 }
}The binding accepts object, view, filter, sort and limit. view names
a saved view whose columns, filter, sort and page size are applied to the render;
filter is additional criteria that AND-combines with the view's rather than
replacing it. See Per-element data binding for the
full table of which blocks honour which keys.
API Reference
createObjectStackAdapter
Factory returning an ObjectStackAdapter ā the concrete adapter class, which
implements DataSource ā backed by an ObjectStack backend.
Config:
function createObjectStackAdapter<T = unknown>(config: {
/** ObjectStack server base URL */
baseUrl: string;
/** Optional bearer token */
token?: string;
/** Optional custom fetch (proxies, auth headers, test doubles) */
fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
/** Metadata cache tuning */
cache?: {
maxSize?: number; // default 100 schemas
ttl?: number; // default 5 * 60 * 1000 ms
};
/** Reconnect behaviour */
autoReconnect?: boolean; // default true
maxReconnectAttempts?: number; // default 3
reconnectDelay?: number; // default 1000 ms
/**
* [ADR-0066] The session's system capabilities, when the host already has them
* at construction time. Most hosts do NOT ā `/me/permissions` resolves after
* the adapter exists ā and push them in later via `setSystemCapabilities`.
* Omit for "unreported".
*/
systemCapabilities?: string[];
}): ObjectStackAdapter<T>;Example:
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.objectstack.dev',
token: 'your-api-token',
cache: { maxSize: 100, ttl: 5 * 60 * 1000 },
autoReconnect: true,
maxReconnectAttempts: 5,
});Pass a record type to get typed results ā the default is unknown:
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
type User = { id: string; name: string; email: string };
const dataSource = createObjectStackAdapter<User>({ baseUrl: 'https://api.example.com' });ObjectStackAdapter
The class behind the factory ā and the type the factory declares. new ObjectStackAdapter(config) and createObjectStackAdapter(config) take the same
config and produce the same type, so the two forms are interchangeable:
import { createObjectStackAdapter, ObjectStackAdapter } from '@object-ui/data-objectstack';
type User = { id: string; name: string; email: string };
// Same declared type, either way.
const fromFactory = createObjectStackAdapter<User>({ baseUrl: 'https://api.example.com' });
const fromClass = new ObjectStackAdapter<User>({ baseUrl: 'https://api.example.com' });What changed. This section used to tell you to hold the class type to
reach the members listed under Beyond DataSource below, because the factory
declared DataSource<T> while returning new ObjectStackAdapter(config): the
value always carried those members, only the declared type hid them
(objectui#7323). That distinction is gone. If you switched to
new ObjectStackAdapter(...) solely to reach a cache, connection-state or batch
method, you can switch back to the factory ā nothing about the value changes, and
neither does its type. The class name is still worth importing when you need
something to annotate with.
Narrowing still works, and is still the right shape for a prop, a field or a test double that must accept any adapter:
import type { DataSource } from '@object-ui/types';
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
// The universal surface only, by annotation.
const dataSource: DataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });On the DataSource interface (available from either form, and from any other
adapter):
find(resource, params?)- Query multiple recordsfindOne(resource, id, params?)- Get a single record by IDcreate(resource, data)- Create a recordupdate(resource, id, data, opts?)- Update a recorddelete(resource, id, opts?)- Delete a recordgetObjectSchema(objectName)- Fetch schema metadata (cached)bulk?(resource, operation, data)- Batch create/update/delete on one objectbatchTransaction?(operations)- Cross-object atomic batch (master-detail)onMutation?(listener)- Subscribe to create/update/delete events
bulk, batchTransaction and onMutation are optional members of
DataSource: not every adapter implements them, so through a DataSource-typed
value they must be feature-detected (typeof dataSource.bulk === 'function').
ObjectStackAdapter implements all three unconditionally, and the factory
declares the class, so a value from either form calls them directly. (onMutation
was listed under Adapter-only here until objectui#7323; it is optional on
DataSource, not absent from it.)
Beyond DataSource (declared on ObjectStackAdapter, so reachable from
either form):
connect()- Establish the connection (called lazily by every operation)getCacheStats()/invalidateCache(key?)/clearCache()- Cache controlgetClient()- Access the underlying@objectstack/clientinstancegetConnectionState()/isConnected()- Connection introspectiononConnectionStateChange(listener)- Subscribe to state changes (returns unsubscribe)onBatchProgress(listener)- Subscribe to bulk progress (returns unsubscribe)
Those six bullets cover nine members, and they are the documented subset.
ObjectStackAdapter declares 20 members beyond DataSource in all
(Exclude<keyof ObjectStackAdapter<unknown>, keyof DataSource<unknown>>); the
other eleven are lower-level seams ā client discovery, cache-key invalidation,
dataset and dashboard access, advisory subscriptions and capability configuration
among them ā which this page does not document and which carry no compatibility
promise here.
Per-element data binding
A schema node's dataSource key is the spec's ElementDataSource (validated by
ElementDataSourceSchema in @objectstack/spec) ā plain JSON metadata, not
the adapter object:
interface ElementDataSource {
/** Object to query (required) */
object: string
/** Saved view name ā supplies columns, filter, sort and page size */
view?: string
/** Additional filter criteria; AND-combines with the view's filter */
filter?: FilterCondition
/** Sort order ā overrides the view's */
sort?: Array<{ field: string; order: 'asc' | 'desc' }>
/** Row cap ā overrides the view's page size */
limit?: number
}The spec schema is strict: a key outside this list is rejected rather than
silently ignored. SchemaRenderer also strips this key from the props it
spreads, so the metadata can never shadow the injected adapter. Full per-block
support table: Per-element data binding.
Usage Examples
Every example below binds a block with the per-element dataSource metadata. The
adapter itself is injected once, at the renderer boundary, as shown in Quick Start.
Grid over an object
{
"type": "object-grid",
"dataSource": { "object": "product", "limit": 20 }
}Narrowing a saved view
filter AND-combines with the view's own filter, so a binding can only narrow
what the view already restricts:
{
"type": "object-grid",
"dataSource": {
"object": "order",
"view": "open_orders",
"filter": { "total": { "$gt": 100 } },
"limit": 50
}
}Form on one record
Form blocks honour object (a form edits a record, so there is no collection
query to filter or page):
{
"type": "object-form",
"dataSource": { "object": "user" }
}Kanban
object-kanban honours object and filter; it has no ordering or row cap of
its own:
{
"type": "object-kanban",
"dataSource": { "object": "task", "filter": { "project": "acme" } }
}Configuration
The adapter reads no environment variables of its own ā configuration is
passed to createObjectStackAdapter in code. Sourcing those values from the
environment is your bundler's business (process.env.* under Node or webpack,
import.meta.env.* under Vite):
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
const dataSource = createObjectStackAdapter({
baseUrl: process.env.OBJECTSTACK_API_URL!,
token: process.env.OBJECTSTACK_API_TOKEN,
cache: { maxSize: 200, ttl: 10 * 60 * 1000 },
autoReconnect: true,
maxReconnectAttempts: 5,
reconnectDelay: 2000,
});Custom fetch
Pass your own fetch to route requests through a proxy or attach extra headers:
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
const dataSource = createObjectStackAdapter({
baseUrl: 'https://api.objectstack.dev',
fetch: (input, init) =>
fetch(input, { ...init, headers: { ...init?.headers, 'X-Tenant': 'acme' } }),
});Advanced Usage
Custom Queries
Call the adapter directly for queries a block does not cover. Query parameters
are OData-style ($filter, $orderby, $top, $skip, $select, $expand):
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });
const startDate = '2024-01-01';
const [users, orders] = await Promise.all([
dataSource.find('user', {
$filter: { createdAt: { $gte: startDate } },
$orderby: 'createdAt desc',
$top: 20,
}),
dataSource.find('order', {
$filter: { status: 'completed' },
$select: ['id', 'total', 'createdAt'],
}),
]);
// find() resolves to { data, total? }
console.log(users.data.length, users.total);Mutations
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
type User = { id: string; name: string; email: string };
const dataSource = createObjectStackAdapter<User>({ baseUrl: 'https://api.example.com' });
const user = await dataSource.create('user', { name: 'Ada', email: 'ada@example.com' });
const updated = await dataSource.update('user', user.id, { name: 'Ada Lovelace' });
await dataSource.delete('user', user.id);Batch writes on one object go through bulk, and cross-object writes that must
commit or roll back together go through batchTransaction. Both are optional on
the DataSource interface, so a value you annotated as DataSource still has to
feature-detect them ā but the adapter implements both unconditionally and the
factory declares the adapter, so the value from Quick Start calls them directly:
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
type User = { id: string; name: string; email: string };
const adapter = createObjectStackAdapter<User>({ baseUrl: 'https://api.example.com' });
await adapter.bulk('user', 'create', [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' },
]);
// `{ $ref: 0 }` resolves to the id minted by operation 0
await adapter.batchTransaction([
{ object: 'invoice', action: 'create', data: { no: 'INV-1' } },
{ object: 'invoice_line', action: 'create', data: { invoice: { $ref: 0 }, amount: 10 } },
]);Error Handling
The adapter throws typed errors with stable codes:
import {
createObjectStackAdapter,
MetadataNotFoundError,
AuthenticationError,
ConnectionError,
} from '@object-ui/data-objectstack';
const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });
try {
const schema = await dataSource.getObjectSchema('user');
} catch (error) {
if (error instanceof MetadataNotFoundError) {
// code 'METADATA_NOT_FOUND', statusCode 404
} else if (error instanceof AuthenticationError) {
// code 'AUTHENTICATION_ERROR', statusCode 401
} else if (error instanceof ConnectionError) {
// code 'CONNECTION_ERROR', statusCode 503
}
}Exported error types: ObjectStackError (base), MetadataNotFoundError,
BulkOperationError, ConnectionError, AuthenticationError,
DataApiValidationError, plus the isObjectStackError / isErrorType guards.
Package Information
Package Name: @object-ui/data-objectstack ā published on npm, see the
npm page for the current version
License: MIT
Dependencies
This package declares no peer dependencies. Everything below is a regular dependency that is installed and resolved with the package:
- @objectstack/client - ObjectStack API client
- @objectstack/spec - ObjectStack metadata spec and shared contracts
- @object-ui/core - Schema engine and query helpers
- @object-ui/types - TypeScript types
Integration with Plugins
Many plugins support ObjectStack data sources:
- plugin-grid - Data grids with ObjectStack queries
- plugin-form - Forms with ObjectStack CRUD
- plugin-kanban - Kanban boards with ObjectStack data
- plugin-calendar - Calendars with ObjectStack events
- plugin-gantt - Gantt charts with ObjectStack tasks
- plugin-map - Maps with ObjectStack locations
Next Steps
- ObjectStack Documentation - Learn about ObjectStack
- Data Sources Guide - Learn about data sources
- Plugins - Explore plugins with data support
- Schema Overview - Learn the schema format
Troubleshooting
Authentication Errors
An AuthenticationError (code AUTHENTICATION_ERROR, status 401) means the
token passed to createObjectStackAdapter was missing, expired or rejected.
Check the connection state and the values you passed in:
Connection introspection lives on the adapter, not on the DataSource interface
ā and the factory declares the adapter, so the value from Quick Start reaches it
without a second construction:
import { createObjectStackAdapter } from '@object-ui/data-objectstack';
const dataSource = createObjectStackAdapter({ baseUrl: 'https://api.example.com' });
console.log(dataSource.getConnectionState()); // 'connected' | 'error' | ...
dataSource.onConnectionStateChange((event) => {
if (event.error) console.error('Connection error:', event.error);
});CORS Issues
Configure CORS on your ObjectStack backend to allow your domain.
Data Not Loading
Check browser console and network tab for errors. Verify:
baseUrlpoints at the right ObjectStack server- The token is valid and not expired
- The object name exists in your ObjectStack instance
- The block's
dataSourcebinding names an object (and aview, if used) that resolves - An adapter is actually injected ā a block with no
SchemaRendererProviderabove it and nodataSourceprop has nothing to query with