npm package: @objectql/sdk
Description: Remote HTTP driver for ObjectQL - Universal client for browser, Node.js, and edge runtimes with DriverInterface v4.0 compliance
Latest version: 4.2.2

# @objectql/sdk

> **Remote HTTP Driver for ObjectQL** - Universal client for browser, Node.js, and edge runtimes

[![License](https://img.shields.io/badge/license-MIT-blue.svg)](../../../LICENSE)
[![TypeScript](https://img.shields.io/badge/written%20in-TypeScript-3178C6.svg)](https://www.typescriptlang.org/)

The `@objectql/sdk` package provides a type-safe HTTP client for ObjectQL servers. It works seamlessly in browsers, Node.js, Deno, and edge runtimes like Cloudflare Workers.

---

## ✨ Features

* **🌍 Universal Runtime** - Works in browsers, Node.js, Deno, and edge environments
* **📦 Zero Dependencies** - Only depends on `@objectql/types` and `@objectstack/spec` for type definitions
* **🔒 Type-Safe** - Full TypeScript support with generics
* **🚀 Modern APIs** - Uses native `fetch` API available in all modern JavaScript runtimes
* **🎯 RESTful Interface** - Clean, predictable API design
* **✅ DriverInterface v4.0** - Fully compliant with ObjectStack protocol specification
* **🔐 Authentication** - Built-in support for Bearer tokens and API keys
* **🔄 Retry Logic** - Automatic retry with exponential backoff for network resilience
* **📊 Request Logging** - Optional request/response logging for debugging

---

## 📦 Installation

```bash
npm install @objectql/sdk @objectql/types
```

For frontend projects:
```bash
# Using npm
npm install @objectql/sdk @objectql/types

# Using yarn
yarn add @objectql/sdk @objectql/types

# Using pnpm
pnpm add @objectql/sdk @objectql/types
```

---

## 🚀 Quick Start

### Browser Usage (ES Modules)

```html
<!DOCTYPE html>
<html>
<head>
    <title>ObjectQL SDK Browser Example</title>
</head>
<body>
    <h1>ObjectQL Browser Client</h1>
    <div id="users"></div>

    <script type="module">
        // Option 1: Using unpkg CDN
        import { DataApiClient } from 'https://unpkg.com/@objectql/sdk/dist/index.js';

        // Option 2: Using a bundler (Vite, Webpack, etc.)
        // import { DataApiClient } from '@objectql/sdk';

        const client = new DataApiClient({
            baseUrl: 'http://localhost:3000',
            token: 'your-auth-token' // Optional
        });

        // Fetch and display users
        async function loadUsers() {
            const response = await client.list('users', {
                filter: [['status', '=', 'active']],
                limit: 10
            });

            const usersDiv = document.getElementById('users');
            usersDiv.innerHTML = response.items
                .map(user => `<p>${user.name} - ${user.email}</p>`)
                .join('');
        }

        loadUsers().catch(console.error);
    </script>
</body>
</html>
```

### React / Vue / Angular

```typescript
import { DataApiClient, MetadataApiClient } from '@objectql/sdk';

// Initialize clients
const dataClient = new DataApiClient({
    baseUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000',
    token: localStorage.getItem('auth_token')
});

const metadataClient = new MetadataApiClient({
    baseUrl: process.env.REACT_APP_API_URL || 'http://localhost:3000'
});

// Use in your components
async function fetchUsers() {
    const response = await dataClient.list('users', {
        filter: [['status', '=', 'active']],
        sort: [['created_at', 'desc']]
    });
    return response.items;
}
```

### Node.js

```javascript
const { DataApiClient } = require('@objectql/sdk');

const client = new DataApiClient({
    baseUrl: 'http://localhost:3000'
});

async function main() {
    const users = await client.list('users');
    console.log(users.items);
}

main();
```

---

## 📚 API Reference

### DataApiClient

Client for CRUD operations on data records.

#### Constructor

```typescript
new DataApiClient(config: DataApiClientConfig)
```

**Config Options:**
* `baseUrl` (string, required) - Base URL of the ObjectQL server
* `token` (string, optional) - Authentication token
* `headers` (Record<string, string>, optional) - Additional HTTP headers
* `timeout` (number, optional) - Request timeout in milliseconds (default: 30000)

#### Methods

##### `list<T>(objectName: string, params?: DataApiListParams): Promise<DataApiListResponse<T>>`

List records with optional filtering, sorting, and pagination.

```typescript
const users = await client.list('users', {
    filter: [['status', '=', 'active']],
    sort: [['name', 'asc']],
    limit: 20,
    skip: 0,
    fields: ['name', 'email', 'status']
});
```

##### `get<T>(objectName: string, id: string | number): Promise<DataApiItemResponse<T>>`

Get a single record by ID.

```typescript
const user = await client.get('users', 'user_123');
```

##### `create<T>(objectName: string, data: DataApiCreateRequest): Promise<DataApiItemResponse<T>>`

Create a new record.

```typescript
const newUser = await client.create('users', {
    name: 'Alice',
    email: 'alice@example.com',
    status: 'active'
});
```

##### `createMany<T>(objectName: string, data: DataApiCreateManyRequest): Promise<DataApiListResponse<T>>`

Create multiple records at once.

```typescript
const newUsers = await client.createMany('users', [
    { name: 'Bob', email: 'bob@example.com' },
    { name: 'Charlie', email: 'charlie@example.com' }
]);
```

##### `update<T>(objectName: string, id: string | number, data: DataApiUpdateRequest): Promise<DataApiItemResponse<T>>`

Update an existing record.

```typescript
const updated = await client.update('users', 'user_123', {
    status: 'inactive'
});
```

##### `updateMany(objectName: string, request: DataApiBulkUpdateRequest): Promise<DataApiResponse>`

Update multiple records matching filters.

```typescript
await client.updateMany('users', {
    filters: [['status', '=', 'pending']],
    data: { status: 'active' }
});
```

##### `delete(objectName: string, id: string | number): Promise<DataApiDeleteResponse>`

Delete a record by ID.

```typescript
await client.delete('users', 'user_123');
```

##### `deleteMany(objectName: string, request: DataApiBulkDeleteRequest): Promise<DataApiDeleteResponse>`

Delete multiple records matching filters.

```typescript
await client.deleteMany('users', {
    filters: [['created_at', '<', '2023-01-01']]
});
```

##### `count(objectName: string, filters?: FilterExpression): Promise<DataApiCountResponse>`

Count records matching filters.

```typescript
const result = await client.count('users', [['status', '=', 'active']]);
console.log(result.count);
```

---

### MetadataApiClient

Client for reading object schemas and metadata.

#### Constructor

```typescript
new MetadataApiClient(config: MetadataApiClientConfig)
```

#### Methods

##### `listObjects(): Promise<MetadataApiObjectListResponse>`

List all available objects.

```typescript
const objects = await metadataClient.listObjects();
```

##### `getObject(objectName: string): Promise<MetadataApiObjectDetailResponse>`

Get detailed schema for an object.

```typescript
const userSchema = await metadataClient.getObject('users');
console.log(userSchema.fields);
```

##### `getField(objectName: string, fieldName: string): Promise<FieldMetadataResponse>`

Get metadata for a specific field.

```typescript
const emailField = await metadataClient.getField('users', 'email');
```

##### `listActions(objectName: string): Promise<MetadataApiActionsResponse>`

List actions available for an object.

```typescript
const actions = await metadataClient.listActions('users');
```

---

### RemoteDriver (DriverInterface v4.0)

Client for connecting to a remote ObjectQL server via HTTP. Implements the standard `DriverInterface` from `@objectstack/spec`.

#### Constructor

```typescript
// Legacy constructor
new RemoteDriver(baseUrl: string, rpcPath?: string)

// New config-based constructor (recommended)
new RemoteDriver(config: SdkConfig)
```

**Config Options:**
* `baseUrl` (string, required) - Base URL of the ObjectQL server
* `rpcPath` (string, optional) - JSON-RPC endpoint path (default: /api/objectql)
* `queryPath` (string, optional) - Query endpoint path (default: /api/query)
* `commandPath` (string, optional) - Command endpoint path (default: /api/command)
* `executePath` (string, optional) - Custom execute endpoint path (default: /api/execute)
* `token` (string, optional) - Authentication token (Bearer)
* `apiKey` (string, optional) - API key for authentication
* `headers` (Record<string, string>, optional) - Custom HTTP headers
* `timeout` (number, optional) - Request timeout in milliseconds (default: 30000)
* `enableRetry` (boolean, optional) - Enable automatic retry on failure (default: false)
* `maxRetries` (number, optional) - Maximum retry attempts (default: 3)
* `enableLogging` (boolean, optional) - Enable request/response logging (default: false)

#### Methods

##### `executeQuery(ast: QueryAST, options?: any): Promise<{ value: any[]; count?: number }>`

Execute a query using QueryAST format (DriverInterface v4.0).

```typescript
import { RemoteDriver } from '@objectql/sdk';

const driver = new RemoteDriver({
    baseUrl: 'http://localhost:3000',
    token: 'your-auth-token',
    enableRetry: true,
    maxRetries: 3
});

// Execute a QueryAST
const result = await driver.executeQuery({
    object: 'users',
    fields: ['name', 'email', 'status'],
    filters: {
        type: 'comparison',
        field: 'status',
        operator: '=',
        value: 'active'
    },
    sort: [{ field: 'created_at', order: 'desc' }],
    top: 10,
    skip: 0
});

console.log(result.value); // Array of users
console.log(result.count); // Total count
```

##### `executeCommand(command: Command, options?: any): Promise<CommandResult>`

Execute a command for mutation operations (create, update, delete, bulk operations).

```typescript
// Create a record
const createResult = await driver.executeCommand({
    type: 'create',
    object: 'users',
    data: {
        name: 'Alice',
        email: 'alice@example.com',
        status: 'active'
    }
});

console.log(createResult.success); // true
console.log(createResult.data); // Created record
console.log(createResult.affected); // 1

// Update a record
const updateResult = await driver.executeCommand({
    type: 'update',
    object: 'users',
    id: 'user_123',
    data: { status: 'inactive' }
});

// Delete a record
const deleteResult = await driver.executeCommand({
    type: 'delete',
    object: 'users',
    id: 'user_123'
});

// Bulk create
const bulkCreateResult = await driver.executeCommand({
    type: 'bulkCreate',
    object: 'users',
    records: [
        { name: 'Bob', email: 'bob@example.com' },
        { name: 'Charlie', email: 'charlie@example.com' }
    ]
});

// Bulk update
const bulkUpdateResult = await driver.executeCommand({
    type: 'bulkUpdate',
    object: 'users',
    updates: [
        { id: 'user_1', data: { status: 'active' } },
        { id: 'user_2', data: { status: 'inactive' } }
    ]
});

// Bulk delete
const bulkDeleteResult = await driver.executeCommand({
    type: 'bulkDelete',
    object: 'users',
    ids: ['user_1', 'user_2', 'user_3']
});
```

##### `execute(endpoint?: string, payload?: any, options?: any): Promise<any>`

Execute a custom operation on the remote server.

```typescript
// Execute a custom workflow
const workflowResult = await driver.execute('/api/workflows/approve', {
    workflowId: 'wf_123',
    comment: 'Approved by manager'
});

// Use default execute endpoint
const customResult = await driver.execute(undefined, {
    action: 'calculateMetrics',
    params: { year: 2024, quarter: 'Q1' }
});

// Call a custom action
const actionResult = await driver.execute('/api/actions/send-email', {
    to: 'user@example.com',
    subject: 'Welcome',
    body: 'Welcome to our platform!'
});
```

#### Legacy CRUD Methods

The RemoteDriver also supports legacy CRUD methods for backward compatibility:

```typescript
// Find records
const users = await driver.find('users', {
    filters: [['status', '=', 'active']],
    sort: [['name', 'asc']],
    limit: 10
});

// Find one record
const user = await driver.findOne('users', 'user_123');

// Create a record
const newUser = await driver.create('users', {
    name: 'Alice',
    email: 'alice@example.com'
});

// Update a record
const updated = await driver.update('users', 'user_123', {
    status: 'inactive'
});

// Delete a record
await driver.delete('users', 'user_123');

// Count records
const count = await driver.count('users', {
    filters: [['status', '=', 'active']]
});
```

### Authentication Examples

```typescript
// Bearer token authentication
const driverWithToken = new RemoteDriver({
    baseUrl: 'http://localhost:3000',
    token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
});

// API key authentication
const driverWithApiKey = new RemoteDriver({
    baseUrl: 'http://localhost:3000',
    apiKey: 'sk-1234567890abcdef'
});

// Both token and API key
const driverWithBoth = new RemoteDriver({
    baseUrl: 'http://localhost:3000',
    token: 'jwt-token',
    apiKey: 'api-key'
});

// Custom headers
const driverWithHeaders = new RemoteDriver({
    baseUrl: 'http://localhost:3000',
    headers: {
        'X-Tenant-ID': 'tenant_123',
        'X-Request-ID': crypto.randomUUID()
    }
});
```

### Error Handling and Retry

```typescript
import { ObjectQLError, ApiErrorCode } from '@objectql/types';

// Enable retry with exponential backoff
const driver = new RemoteDriver({
    baseUrl: 'http://localhost:3000',
    enableRetry: true,
    maxRetries: 3,
    timeout: 10000
});

try {
    const result = await driver.executeQuery({ object: 'users' });
} catch (error) {
    if (error instanceof ObjectQLError) {
        switch (error.code) {
            case ApiErrorCode.UNAUTHORIZED:
                console.error('Authentication required');
                break;
            case ApiErrorCode.VALIDATION_ERROR:
                console.error('Validation failed:', error.details);
                break;
            case ApiErrorCode.NOT_FOUND:
                console.error('Resource not found');
                break;
            default:
                console.error('API error:', error.message);
        }
    }
}
```

### Request Logging

```typescript
// Enable logging for debugging
const driver = new RemoteDriver({
    baseUrl: 'http://localhost:3000',
    enableLogging: true
});

// Logs will be printed to console:
// [RemoteDriver] executeQuery { endpoint: '...', ast: {...} }
// [RemoteDriver] executeQuery response { value: [...], count: 10 }
```

---

## 🌐 Browser Compatibility

The SDK uses modern JavaScript APIs available in all current browsers:

* **fetch API** - Available in all modern browsers
* **Promises/async-await** - ES2017+ 
* **AbortSignal.timeout()** - Chrome 103+, Firefox 100+, Safari 16.4+

### Automatic Polyfill

The SDK **automatically includes a polyfill** for `AbortSignal.timeout()` that activates when running in older browsers. You don't need to add any polyfills manually - the SDK works universally out of the box!

The polyfill is lightweight and only adds the missing functionality when needed, ensuring compatibility with:
- Chrome 90+
- Firefox 90+
- Safari 15+
- Edge 90+

For even older browsers, you may need to add polyfills for:
- `fetch` API (via `whatwg-fetch`)
- `AbortController` (via `abort-controller` package)

---

## 🔧 Advanced Usage

### Custom Headers

```typescript
const client = new DataApiClient({
    baseUrl: 'http://localhost:3000',
    headers: {
        'X-Custom-Header': 'value',
        'X-Request-ID': crypto.randomUUID()
    }
});
```

### Dynamic Token Updates

```typescript
class AuthenticatedClient {
    private client: DataApiClient;

    constructor(baseUrl: string) {
        this.client = new DataApiClient({ baseUrl });
    }

    setToken(token: string) {
        this.client = new DataApiClient({
            baseUrl: this.client['baseUrl'],
            token
        });
    }

    async fetchData() {
        return this.client.list('users');
    }
}
```

### Error Handling

```typescript
import { ObjectQLError, ApiErrorCode } from '@objectql/types';

try {
    await client.create('users', { email: 'invalid' });
} catch (error) {
    if (error instanceof ObjectQLError) {
        console.error('ObjectQL Error:', error.code, error.message);
        
        if (error.code === ApiErrorCode.VALIDATION_ERROR) {
            console.log('Validation failed:', error.details);
        }
    }
}
```

---

## 📖 Examples

### React Hook

```typescript
import { useState, useEffect } from 'react';
import { DataApiClient } from '@objectql/sdk';

const client = new DataApiClient({
    baseUrl: process.env.REACT_APP_API_URL
});

export function useObjectData<T>(objectName: string, params?: any) {
    const [data, setData] = useState<T[]>([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<Error | null>(null);

    useEffect(() => {
        async function fetchData() {
            try {
                setLoading(true);
                const response = await client.list<T>(objectName, params);
                setData(response.items || []);
            } catch (err) {
                setError(err as Error);
            } finally {
                setLoading(false);
            }
        }

        fetchData();
    }, [objectName, JSON.stringify(params)]);

    return { data, loading, error };
}
```

### Vue Composable

```typescript
import { ref, watchEffect } from 'vue';
import { DataApiClient } from '@objectql/sdk';

const client = new DataApiClient({
    baseUrl: import.meta.env.VITE_API_URL
});

export function useObjectData<T>(objectName: string, params?: any) {
    const data = ref<T[]>([]);
    const loading = ref(true);
    const error = ref<Error | null>(null);

    watchEffect(async () => {
        try {
            loading.value = true;
            const response = await client.list<T>(objectName, params);
            data.value = response.items || [];
        } catch (err) {
            error.value = err as Error;
        } finally {
            loading.value = false;
        }
    });

    return { data, loading, error };
}
```

---

## 🏗️ Architecture

The SDK is designed with the ObjectQL "Trinity" architecture:

1. **@objectql/types** (The Contract) - Pure TypeScript interfaces
2. **@objectql/sdk** (The Client) - HTTP communication layer
3. **ObjectQL Server** (The Backend) - Data processing and storage

```
┌─────────────────┐
│   Frontend      │
│  (Browser/App)  │
│                 │
│  @objectql/sdk  │
└────────┬────────┘
         │ HTTP/REST
         │
┌────────▼────────┐
│ ObjectQL Server │
│                 │
│  @objectql/core │
└────────┬────────┘
         │
    ┌────┴────┐
    │ SQL/Mongo│
    └─────────┘
```

---

## 📄 License

MIT License - see [LICENSE](../../../LICENSE) file for details.

---

## 🔗 Related Packages

* **[@objectql/types](../../foundation/types)** - TypeScript type definitions
* **[@objectql/core](../../foundation/core)** - Core ObjectQL engine
* **@objectstack/plugin-hono-server** - HTTP server (via ObjectStack Kernel)

---

## 🤝 Contributing

We welcome contributions! Please see the main [repository README](../../../README.md) for guidelines.

---

## 📚 Documentation

For complete documentation, visit:
* [Client SDK Guide](../../../docs/api/client-sdk.md)
* [REST API Reference](../../../docs/api/rest.md)
* [ObjectQL Documentation](https://github.com/objectstack-ai/objectql)