ObjectStackObjectStack

Client SDK

The official TypeScript client for ObjectStack — auto-discovery, typed metadata, CRUD, batch operations, and service-aware feature detection.

Client SDK

The @objectstack/client is the official TypeScript client for ObjectStack. It provides a typed, protocol-aware interface that automatically adapts to your server's available services.

Features

  • Auto-Discovery: Connects to your ObjectStack server and discovers available services, routes, and capabilities
  • Service-Aware: Checks discovery.services to determine which features are available before calling them
  • 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/upsert/delete with transaction support
  • Query Builder: Programmatic query construction with createQuery() and createFilter()
  • Standardized Errors: Machine-readable error codes with retry guidance
  • 100% Protocol Compliant: Implements all 15 API namespaces defined in @objectstack/spec

Installation

pnpm add @objectstack/client

Quick Start

import { ObjectStackClient } from '@objectstack/client';

const client = new ObjectStackClient({
  baseUrl: 'http://localhost:3004',
});

async function main() {
  // 1. Connect — fetches discovery manifest
  await client.connect();

  // 2. Check available services
  console.log('Services:', client.discovery?.services);
  // → { metadata: { enabled: true, status: 'degraded' }, data: { enabled: true, status: 'available' }, auth: { enabled: false, ... } }

  // 3. Query data
  const tasks = await client.data.find('todo_task', {
    select: ['subject', 'priority'],
    filters: ['priority', '>=', 2],
    sort: ['-priority'],
    top: 10
  });

  // 4. Create a record
  const newTask = await client.data.create('todo_task', {
    subject: 'New Task',
    priority: 1
  });

  // 5. Batch operations
  const result = await client.data.batch('todo_task', {
    operation: 'update',
    records: [
      { id: '1', data: { status: 'active' } },
      { id: '2', data: { status: 'active' } }
    ],
    options: { atomic: true, returnRecords: true }
  });
  console.log(`Updated ${result.succeeded} records`);
}

Auto-Discovery

When you call client.connect(), the client:

  1. Fetches /.well-known/objectstack (or falls back to /api/v1)
  2. Parses the discovery response including routes, features, and services
  3. Configures all API route paths dynamically
await client.connect();

// Discovery result is available
const discovery = client.discovery;
console.log(discovery.version);      // "1.0.0"
console.log(discovery.environment);  // "development"

// Check if a service is available before using it
if (discovery.services?.auth?.enabled) {
  // Auth plugin is installed — login is available
  await client.auth.login({ username: 'admin', password: 'secret' });
} else {
  console.log(discovery.services?.auth?.message);
  // → "Install an auth plugin to enable"
}

Plugin-driven services: Auth, workflow, automation, AI, and other services are only available when the corresponding plugin is installed. Always check discovery.services before calling plugin-provided API methods.


Protocol Coverage

The @objectstack/client SDK aims to implement the ObjectStack API protocol specification. It covers all 15 API namespaces defined in @objectstack/spec:

NamespaceStatusMethodsPurpose
discovery1API version & capabilities detection
meta7Metadata operations (objects, views, plugins)
data10CRUD & query operations
auth5Authentication & user management
permissions3Access control checks
packages6Plugin/package lifecycle management
views5UI view definitions
workflow5Workflow state transitions
analytics3Analytics queries
automation1Automation triggers
storage2File upload & download
i18n3Internationalization
notifications7Push notifications
realtime6WebSocket subscriptions
ai4AI services (NLQ, chat, insights)

Protocol compliance & verification: See CLIENT_SPEC_COMPLIANCE.md for detailed method-by-method verification and CLIENT_SERVER_INTEGRATION_TESTS.md for comprehensive integration test specifications.


API Namespaces

client.meta — Metadata

// Get object schema
const accountSchema = await client.meta.getObject('account');
console.log(accountSchema.fields);

// List all metadata types
const types = await client.meta.getTypes();
// → { types: ['object', 'view', 'plugin', ...] }

// Get with ETag caching
const cached = await client.meta.getCached('account', {
  ifNoneMatch: '"686897696a7c876b7e"'
});
if (cached.notModified) {
  console.log('Using cached metadata');
}

// Get auto-generated view
const listView = await client.meta.getView('account', 'list');

client.data — CRUD & Batch

// Find with query options
const accounts = await client.data.find('account', {
  select: ['name', 'industry', 'revenue'],
  filters: ['industry', '=', 'Technology'],
  sort: ['-revenue'],
  top: 20,
  skip: 0,
});

// Get by ID
const account = await client.data.get('account', '123');

// Create
const created = await client.data.create('account', {
  name: 'Acme Corp',
  industry: 'Technology',
});

// Update (partial)
const updated = await client.data.update('account', '123', {
  industry: 'Healthcare',
});

// Delete
await client.data.delete('account', '123');

// Batch operation (recommended for bulk)
const batchResult = await client.data.batch('account', {
  operation: 'upsert',
  records: [
    { id: '1', data: { name: 'Acme', status: 'active' } },
    { id: '2', data: { name: 'Beta', status: 'active' } },
  ],
  options: {
    atomic: true,
    returnRecords: true,
    continueOnError: false,
    validateOnly: false,
  },
});

// Convenience bulk methods
await client.data.createMany('account', [{ name: 'A' }, { name: 'B' }]);
await client.data.updateMany('account', [
  { id: '1', data: { status: 'closed' } },
  { id: '2', data: { status: 'closed' } },
]);
await client.data.deleteMany('account', ['1', '2', '3']);

client.analytics — BI Queries

// Semantic analytics query (cube-style)
const result = await client.analytics.query({
  cube: 'account',
  measures: ['revenue.sum', 'count'],
  dimensions: ['industry'],
  filters: [{ member: 'status', operator: 'equals', values: ['active'] }],
  limit: 100,
});

// Get cube metadata for a specific cube
const meta = await client.analytics.meta('account');

// Explain a query plan
const explained = await client.analytics.explain({
  cube: 'account',
  measures: ['revenue.sum'],
});

client.packages — Package Management

const packages = await client.packages.list();
await client.packages.install({ name: 'plugin-auth', version: '1.0.0' });
await client.packages.enable('plugin-auth');
await client.packages.disable('plugin-auth');
await client.packages.uninstall('plugin-auth');

Additional Namespaces

The client also provides full implementations for:

// Auth — User authentication and session management
await client.auth.login({ email: 'user@example.com', password: 'pass' });
await client.auth.register({ email: 'new@example.com', password: 'pass' });
await client.auth.me();
await client.auth.logout();
await client.auth.refreshToken('refresh-token-string');

// Permissions — Access control checks
await client.permissions.check({ object: 'account', action: 'create' });
await client.permissions.getObjectPermissions('account');
await client.permissions.getEffectivePermissions();

// Workflow — State machine management
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 — WebSocket subscriptions
await client.realtime.connect({ protocol: 'websocket' });
await client.realtime.subscribe({ channel: 'account', event: 'update' });
await client.realtime.unsubscribe('subscription-id');
await client.realtime.setPresence('account', { status: 'online' });
await client.realtime.getPresence('account');
await client.realtime.disconnect();

// Notifications — Push notification management
await client.notifications.registerDevice({ token: 'device-token', platform: 'ios' });
await client.notifications.unregisterDevice('device-id');
await client.notifications.getPreferences();
await client.notifications.updatePreferences({ email: true, push: false });
await client.notifications.list({ read: false });
await client.notifications.markRead(['notif-1', 'notif-2']);
await client.notifications.markAllRead();

// AI — AI-powered features
await client.ai.nlq({ query: 'Show me all active accounts' });
await client.ai.chat({ message: 'Summarize this project', context: projectId });
await client.ai.suggest({ object: 'account', field: 'industry' });
await client.ai.insights({ object: 'sales', recordId: dealId });

// i18n — Internationalization
await client.i18n.getLocales();
await client.i18n.getTranslations('zh-CN');
await client.i18n.getFieldLabels('account', 'zh-CN');

// Automation — Trigger workflows and automations
await client.automation.trigger('send_welcome_email', { userId });

// Storage — File upload and management
await client.storage.upload(fileData, 'user');
await client.storage.getDownloadUrl('file-123');

// Views — UI view management
await client.views.list('account');
await client.views.get('account', viewId);
await client.views.create('account', { name: 'my_view', ... });
await client.views.update('account', viewId, { ... });
await client.views.delete('account', viewId);

Service availability: Optional services (workflow, ai, etc.) are only available when the corresponding plugin is installed on the server. Always check client.discovery?.services to verify service availability before calling these methods.


Query Builder

Build complex queries programmatically:

import { createQuery, createFilter } from '@objectstack/client';

const query = createQuery('account')
  .select('name', 'industry', 'revenue')
  .where((f) => {
    f.equals('industry', 'Technology');
    f.greaterThan('revenue', 10000);
  })
  .orderBy('revenue', 'desc')
  .limit(50)
  .build();

const results = await client.data.query('account', query);

Filter Builder Methods

The FilterBuilder provides a rich set of filter methods:

import { createFilter } from '@objectstack/client';

const filter = createFilter()
  .equals('status', 'active')        // field = value
  .notEquals('type', 'archived')      // field != value
  .greaterThan('revenue', 10000)      // field > value
  .lessThanOrEqual('age', 100)        // field <= value
  .in('category', ['A', 'B', 'C'])   // field IN (...)
  .like('name', '%Corp%')             // field LIKE pattern
  .contains('name', 'Corp')           // LIKE %Corp%
  .startsWith('name', 'Acme')         // LIKE Acme%
  .isNull('deleted_at')               // field IS NULL
  .between('created_at', '2024-01', '2024-12')
  .build();

Query Options

The find method accepts an options object:

PropertyTypeDescriptionExample
selectstring[]Fields to retrieve['name', 'email']
filtersMap or ASTFilter criteria['status', '=', 'active']
sortstring or string[]Sort order['-created_at']
topnumberLimit records20
skipnumberOffset for pagination0

Batch Options

PropertyTypeDefaultDescription
atomicbooleantrueRollback entire batch on any failure
returnRecordsbooleanfalseInclude full records in response
continueOnErrorbooleanfalseContinue after errors (when atomic=false)
validateOnlybooleanfalseDry-run mode — validate without persisting

Error Handling

All errors follow a standardized format:

try {
  await client.data.create('todo_task', { subject: '' });
} catch (error) {
  console.error(error.code);       // 'validation_error'
  console.error(error.category);   // 'validation'
  console.error(error.httpStatus); // 400
  console.error(error.retryable);  // false
  console.error(error.details);    // { ... }
}

Error Codes

CodeHTTPCategoryRetryableDescription
validation_error400validationNoInput validation failed
invalid_query400validationNoMalformed query expression
unauthenticated401authNoAuthentication required
permission_denied403authNoInsufficient permissions
resource_not_found404requestNoResource does not exist
rate_limit_exceeded429rate_limitYesToo many requests
internal_error500serverYesUnexpected server error
service_unavailable503serverYesService temporarily unavailable
not_implemented501serverNoService not installed (plugin missing)

Configuration

interface ClientConfig {
  /** ObjectStack server URL */
  baseUrl: string;
  /** Bearer token for authentication (if auth plugin installed) */
  token?: string;
  /** Custom fetch implementation (e.g. for SSR or testing) */
  fetch?: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
  /** Logger instance for debugging */
  logger?: Logger;
  /** Enable debug logging */
  debug?: boolean;
}

React Hooks

For React applications, use @objectstack/client-react:

pnpm add @objectstack/client-react
import { ObjectStackProvider, useClient, useQuery } from '@objectstack/client-react';
import { ObjectStackClient } from '@objectstack/client';

// 1. Wrap your app with the provider
const client = new ObjectStackClient({ baseUrl: 'http://localhost:3004' });

function App() {
  return (
    <ObjectStackProvider client={client}>
      <AccountList />
    </ObjectStackProvider>
  );
}

// 2. Use hooks in child components
function AccountList() {
  const { data, isLoading, error, refetch } = useQuery('account', {
    filters: ['status', '=', 'active'],
    sort: ['-created_at'],
    top: 20,
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  return data?.records.map(a => <div key={a.id}>{a.name}</div>);
}

Available Hooks

HookPurpose
useClient()Access the ObjectStackClient instance from context
useQuery(object, options)Query data with auto caching and refetching
useMutation(object)Create/update/delete mutations
usePagination(object, options)Paginated data queries
useInfiniteQuery(object, options)Infinite scroll queries
useObject(name)Get object metadata
useView(object, type)Get view definition
useFields(object)Get field definitions
useMetadata(type, name)Get arbitrary metadata

Testing

The client SDK includes comprehensive unit and integration tests to ensure reliability and protocol compliance.

Unit Tests

cd packages/client
pnpm test

Unit tests use mocks to verify client behavior without requiring a server.

Integration Tests

Note: Integration tests require a running ObjectStack server. The server is provided by a separate repository and must be set up independently.

# Prerequisite: Start an ObjectStack server with test data
# For example, using the reference server repository
# Follow the server repository's documentation for local setup

# From this repository, run the integration test script
cd packages/client
pnpm test:integration

Integration tests verify end-to-end communication with a live ObjectStack server across all 15 API namespaces.

Test coverage: Integration test specifications cover discovery/connection, authentication, metadata operations, CRUD operations (basic, batch, advanced queries), permissions, workflow, realtime, notifications, AI services, i18n, analytics, packages, views, storage, and automation.


Protocol Compliance Documentation

For detailed information about the client's protocol implementation:


Next Steps

On this page