Skip to content

Latest commit

 

History

History
630 lines (492 loc) · 13.9 KB

File metadata and controls

630 lines (492 loc) · 13.9 KB

Client SDK Usage

Learn how to use the ObjectQL TypeScript client SDK for frontend applications. This comprehensive guide covers SDK installation, configuration, API calls, and type-safe data access from JavaScript/TypeScript clients.

Installation

npm install @objectql/sdk @objectql/types

Overview

The @objectql/sdk package provides two main client classes:

  1. DataApiClient - For CRUD operations on data records
  2. MetadataApiClient - For reading object schemas and metadata

All types are defined in @objectql/types to maintain zero dependencies and enable frontend usage.


Data API Client

Basic Setup

import { DataApiClient } from '@objectql/sdk';

const dataClient = new DataApiClient({
  baseUrl: 'http://localhost:3000',
  token: 'your-auth-token', // Optional
  timeout: 30000 // Optional, defaults to 30s
});

List Records

// Simple list
const response = await dataClient.list('users');
console.log(response.items); // Array of user records
console.log(response.meta);  // Pagination metadata

// With filters and pagination
const activeUsers = await dataClient.list('users', {
  filter: [['status', '=', 'active']],
  sort: [['created_at', 'desc']],
  limit: 20,
  skip: 0,
  fields: ['name', 'email', 'status']
});

// TypeScript with generics
interface User {
  _id: string;
  name: string;
  email: string;
  status: 'active' | 'inactive';
  created_at: string;
}

const users = await dataClient.list<User>('users', {
  filter: [['status', '=', 'active']]
});

users.items?.forEach(user => {
  console.log(user.name); // Type-safe!
});

Get Single Record

const user = await dataClient.get('users', 'user_123');
console.log(user.name);
console.log(user.email);

// With TypeScript types
const user = await dataClient.get<User>('users', 'user_123');

Create Record

// Create single record
const newUser = await dataClient.create('users', {
  name: 'Alice Johnson',
  email: 'alice@example.com',
  role: 'admin'
});

console.log(newUser._id); // Generated ID
console.log(newUser.created_at); // Timestamp

// With TypeScript
const newUser = await dataClient.create<User>('users', {
  name: 'Alice Johnson',
  email: 'alice@example.com',
  status: 'active'
});

Create Multiple Records

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

console.log(newUsers.items); // Array of created users

Update Record

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

console.log(updated.updated_at); // New timestamp

Bulk Update

const result = await dataClient.updateMany('users', {
  filters: [['status', '=', 'pending']],
  data: { status: 'active' }
});

Delete Record

const result = await dataClient.delete('users', 'user_123');
console.log(result.success);

Bulk Delete

const result = await dataClient.deleteMany('users', {
  filters: [['created_at', '<', '2023-01-01']]
});

console.log(result.deleted_count);

Count Records

const countResult = await dataClient.count('users', [
  ['status', '=', 'active']
]);

console.log(countResult.count);

Metadata API Client

Basic Setup

import { MetadataApiClient } from '@objectql/sdk';

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

List All Objects

const objectsResponse = await metadataClient.listObjects();

objectsResponse.items.forEach(obj => {
  console.log(`${obj.name}: ${obj.label}`);
  console.log(`  Icon: ${obj.icon}`);
  console.log(`  Description: ${obj.description}`);
});

Get Object Schema

const userSchema = await metadataClient.getObject('users');

console.log(userSchema.name);
console.log(userSchema.label);
console.log(userSchema.description);

// Iterate through fields
Object.entries(userSchema.fields).forEach(([key, field]) => {
  console.log(`${field.name} (${field.type})`);
  if (field.required) console.log('  - Required');
  if (field.unique) console.log('  - Unique');
});

// Check available actions
if (userSchema.actions) {
  Object.keys(userSchema.actions).forEach(actionName => {
    console.log(`Action: ${actionName}`);
  });
}

Get Field Metadata

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

console.log(emailField.type);        // "email"
console.log(emailField.required);    // true
console.log(emailField.unique);      // true
console.log(emailField.label);       // "Email Address"

List Object Actions

const actionsResponse = await metadataClient.listActions('users');

actionsResponse.items.forEach(action => {
  console.log(`${action.name} (${action.type})`);
  console.log(`  Label: ${action.label}`);
  console.log(`  Description: ${action.description}`);
});

List Custom Metadata

// List all views
const views = await metadataClient.listByType('view');

// List all forms
const forms = await metadataClient.listByType('form');

// List all pages
const pages = await metadataClient.listByType('page');

Get Specific Metadata

const userListView = await metadataClient.getMetadata('view', 'user_list');
console.log(userListView);

const userForm = await metadataClient.getMetadata('form', 'user_create');
console.log(userForm);

Error Handling

All API methods throw errors with structured information:

import { ApiErrorCode } from '@objectql/types';

try {
  const user = await dataClient.get('users', 'invalid_id');
} catch (error) {
  if (error instanceof Error) {
    // Error message format: "ERROR_CODE: Error message"
    console.error(error.message);
    
    if (error.message.includes(ApiErrorCode.NOT_FOUND)) {
      console.log('User not found');
    } else if (error.message.includes(ApiErrorCode.VALIDATION_ERROR)) {
      console.log('Validation failed');
    } else if (error.message.includes(ApiErrorCode.UNAUTHORIZED)) {
      console.log('Authentication required');
    }
  }
}

React Example

Custom Hook for Data Fetching

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

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

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 dataClient.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 };
}

Using the Hook

import { useObjectData } from './hooks/useObjectData';

interface User {
  _id: string;
  name: string;
  email: string;
  status: string;
}

function UserList() {
  const { data: users, loading, error } = useObjectData<User>('users', {
    filter: [['status', '=', 'active']],
    sort: [['name', 'asc']]
  });

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <ul>
      {users.map(user => (
        <li key={user._id}>
          {user.name} - {user.email}
        </li>
      ))}
    </ul>
  );
}

Custom Hook for Metadata

import { useState, useEffect } from 'react';
import { MetadataApiClient, ObjectMetadataDetail } from '@objectql/sdk';

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

export function useObjectSchema(objectName: string) {
  const [schema, setSchema] = useState<ObjectMetadataDetail | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    async function fetchSchema() {
      try {
        setLoading(true);
        const data = await metadataClient.getObject(objectName);
        setSchema(data);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    }

    fetchSchema();
  }, [objectName]);

  return { schema, loading, error };
}

Dynamic Form Generator

import { useObjectSchema } from './hooks/useObjectSchema';

function DynamicForm({ objectName }: { objectName: string }) {
  const { schema, loading, error } = useObjectSchema(objectName);
  
  if (loading) return <div>Loading form...</div>;
  if (error) return <div>Error: {error.message}</div>;
  if (!schema) return null;

  return (
    <form>
      <h2>Create {schema.label}</h2>
      {Object.entries(schema.fields).map(([key, field]) => (
        <div key={key}>
          <label>
            {field.label || field.name}
            {field.required && <span>*</span>}
          </label>
          {field.type === 'text' && <input type="text" name={key} />}
          {field.type === 'email' && <input type="email" name={key} />}
          {field.type === 'number' && (
            <input 
              type="number" 
              name={key}
              min={field.min}
              max={field.max}
            />
          )}
          {field.type === 'select' && (
            <select name={key}>
              {field.options?.map(opt => (
                <option key={opt} value={opt}>{opt}</option>
              ))}
            </select>
          )}
        </div>
      ))}
      <button type="submit">Create</button>
    </form>
  );
}

Vue.js Example

Composable for Data Fetching

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

const dataClient = new DataApiClient({
  baseUrl: import.meta.env.VITE_API_URL || 'http://localhost:3000'
});

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 dataClient.list<T>(objectName, params);
      data.value = response.items || [];
    } catch (err) {
      error.value = err as Error;
    } finally {
      loading.value = false;
    }
  });

  return { data, loading, error };
}

Advanced Filtering

Complex Filter Expressions

// AND condition
const result = await dataClient.list('orders', {
  filter: [
    'and',
    ['status', '=', 'pending'],
    ['total', '>', 100]
  ]
});

// OR condition
const result = await dataClient.list('users', {
  filter: [
    'or',
    ['role', '=', 'admin'],
    ['role', '=', 'manager']
  ]
});

// Nested conditions
const result = await dataClient.list('orders', {
  filter: [
    'and',
    ['status', '=', 'pending'],
    [
      'or',
      ['priority', '=', 'high'],
      ['total', '>', 1000]
    ]
  ]
});

Expanding Relations

const orders = await dataClient.list('orders', {
  expand: {
    customer: {
      fields: ['name', 'email']
    },
    items: {
      fields: ['product_name', 'quantity', 'price']
    }
  }
});

orders.items?.forEach(order => {
  console.log(order.customer.name);
  order.items.forEach(item => {
    console.log(`  - ${item.product_name} x${item.quantity}`);
  });
});

Type Safety Benefits

By using the type definitions from @objectql/types, you get:

  1. Autocomplete - IDEs provide intelligent suggestions
  2. Type Checking - Catch errors at compile time
  3. Documentation - Inline JSDoc comments explain each field
  4. Refactoring - Safely rename and restructure code
import type {
  DataApiListParams,
  DataApiListResponse,
  DataApiItemResponse,
  ApiErrorCode,
  MetadataApiObjectDetailResponse
} from '@objectql/types';

// These types ensure your frontend code stays in sync with the API

Best Practices

  1. Centralize Client Instances

    // api-clients.ts
    export const dataClient = new DataApiClient({
      baseUrl: process.env.API_URL
    });
    
    export const metadataClient = new MetadataApiClient({
      baseUrl: process.env.API_URL
    });
  2. Use Generic Types

    interface Project {
      _id: string;
      name: string;
      status: string;
    }
    
    const projects = await dataClient.list<Project>('projects');
  3. Handle Errors Gracefully

    try {
      await dataClient.create('users', userData);
    } catch (error) {
      // Show user-friendly message
      toast.error('Failed to create user');
    }
  4. Cache Metadata

    // Metadata rarely changes, so cache it
    const schemaCache = new Map();
    
    async function getSchema(objectName: string) {
      if (!schemaCache.has(objectName)) {
        const schema = await metadataClient.getObject(objectName);
        schemaCache.set(objectName, schema);
      }
      return schemaCache.get(objectName);
    }
  5. Use Environment Variables

    const dataClient = new DataApiClient({
      baseUrl: process.env.NEXT_PUBLIC_API_URL,
      token: process.env.NEXT_PUBLIC_API_TOKEN
    });

Summary

The ObjectQL client SDK provides:

  • Type-safe API clients for Data and Metadata operations
  • Zero dependencies in @objectql/types for frontend compatibility
  • RESTful interface matching the server implementation
  • Framework agnostic - works with React, Vue, Angular, etc.
  • Full TypeScript support with generics and inference

For more information, see: