# @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/runtime` over HTTP.

**Key namespaces**:
- `client.meta`: Metadata operations.
- `client.data`: Data operations.

## Installation

```bash
pnpm add @objectstack/client
```

## Usage

```typescript
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 with canonical field names:
- `where`: Filter conditions (WHERE clause). Accepts object or FilterCondition AST.
- `fields`: Array of field names to retrieve (SELECT clause).
- `orderBy`: Sort definition — `'name'`, `['-created_at', 'name']`, or `SortNode[]`.
- `limit`: Maximum number of records to return (LIMIT).
- `offset`: Number of records to skip (OFFSET).
- `expand`: Relations to expand (JOIN / eager-load).

> **Deprecated aliases** (accepted for backward compatibility): `select` → `fields`, `filter`/`filters` → `where`, `sort` → `orderBy`, `top` → `limit`, `skip` → `offset`.

### 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:

```typescript
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:**

```typescript
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](./CLIENT_SPEC_COMPLIANCE.md).

## Available Namespaces

### Complete API Coverage

```typescript
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: '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');

// 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

```bash
pnpm test
```

### Integration Tests

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

```bash
# 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:integration
```

See [CLIENT_SERVER_INTEGRATION_TESTS.md](./CLIENT_SERVER_INTEGRATION_TESTS.md) for detailed test specifications.

