ryuu.js
Version:
Ryuu JavaScript Utility Library
2,171 lines (1,611 loc) • 59.2 kB
Markdown
# ryuu.js
> A powerful JavaScript SDK for building custom applications within the Domo platform
[](https://www.npmjs.com/package/ryuu.js)
[](./LICENSE)
[](https://www.typescriptlang.org/)
ryuu.js (published as `ryuu.js`, developed as `domo.js`) is a comprehensive JavaScript library that enables developers to build interactive custom applications within the Domo platform. It provides seamless communication between your custom app and the Domo environment, supporting data fetching, real-time events, filters, variables, and cross-platform mobile integration.
---
## 📚 Documentation
Choose your version:
- **[v5+ Documentation](#v5-documentation)** ⭐ Latest (recommended)
- **[v4 and Earlier Documentation](#v4-and-earlier-documentation)** 📦 Legacy
---
## v5+ Documentation
## Table of Contents
- [Quick Start](#quick-start)
- [Features](#features)
- [Installation](#installation)
- [Core Concepts](#core-concepts)
- [API Reference](#api-reference)
- [HTTP Methods](#http-methods)
- [Event Listeners](#event-listeners)
- [Emitters](#emitters)
- [Navigation](#navigation)
- [Environment](#environment)
- [Utilities](#utilities)
- [TypeScript Support](#typescript-support)
- [Mobile Platform Support](#mobile-platform-support)
- [Error Handling](#error-handling)
- [Advanced Usage](#advanced-usage)
- [Complete Example](#complete-example)
- [Migration Guide](#migration-guide)
- [Contributing](#contributing)
- [License](#license)
---
## Quick Start
Get started with ryuu.js in just a few lines:
```javascript
import Domo from 'ryuu.js';
// Fetch data from a dataset
const data = await Domo.get('/data/v1/sales');
console.log(data); // Array of objects with your data
// Listen for dataset updates
Domo.onDataUpdated((alias) => {
console.log(`Dataset ${alias} was updated!`);
// Refresh your visualization
});
// Listen for filter changes
Domo.onFiltersUpdated((filters) => {
console.log('Filters changed:', filters);
// Apply filters to your data
});
```
---
## Features
- **HTTP API Access** - Authenticated requests to Domo datasets, datastores, and APIs
- **Real-time Events** - Listen for dataset updates, filter changes, and variable updates
- **Filter Management** - Get and set page-level filters programmatically
- **Variable Management** - Access and update page variables
- **Custom App Data** - Send custom data between apps on the same page
- **Navigation** - Programmatically navigate within Domo
- **Mobile Support** - Full iOS and Android compatibility
- **TypeScript Ready** - Complete type definitions included
- **Zero Dependencies** - Minimal bundle size with no runtime dependencies
- **Extensible** - Override or extend functionality via `extend()` method
---
## Installation
### NPM
```bash
npm install ryuu.js
```
Then import in your application:
```javascript
// ES modules
import Domo from 'ryuu.js';
// CommonJS
const Domo = require('ryuu.js').default;
```
### CDN / Script Tag
```html
<script src="https://unpkg.com/ryuu.js"></script>
<script>
// Domo is available globally
Domo.get('/data/v1/sales').then(data => {
console.log(data);
});
</script>
```
### TypeScript
TypeScript definitions are included automatically:
```typescript
import Domo, { Filter, Variable, RequestOptions } from 'ryuu.js';
```
---
## Core Concepts
### Communication Architecture
Your custom app runs in an iframe within the Domo platform. ryuu.js establishes a bidirectional communication channel using:
- **MessageChannel API** - Primary communication mechanism for desktop/web
- **webkit.messageHandlers** - iOS native integration
- **Global objects** - Android/Flutter integration
### Request-Reply Pattern
Async operations (like updating filters) use an **ASK-ACK-REPLY** pattern:
1. **ASK** - Your app sends a request with a unique ID
2. **ACK** - Parent acknowledges receipt (optional callback)
3. **REPLY** - Parent sends response when operation completes (optional callback)
```javascript
Domo.requestFiltersUpdate(
filters,
true,
() => console.log('Request acknowledged'),
(response) => console.log('Request completed:', response)
);
```
### Environment Context
Access information about the current user and environment via `Domo.env`:
```javascript
console.log(Domo.env.userId); // Current user ID
console.log(Domo.env.customer); // Customer name
console.log(Domo.env.pageId); // Current page ID
console.log(Domo.env.locale); // Locale (e.g., 'en-US')
console.log(Domo.env.platform); // Platform (e.g., 'desktop', 'mobile')
```
**Security Note:** Environment properties come from URL parameters and can be spoofed. Always verify with the API for secure operations:
```javascript
const authenticatedUser = await Domo.get('/domo/environment/v1/');
```
---
## API Reference
### HTTP Methods
All HTTP methods return Promises and support multiple data formats.
#### `Domo.get(url, options?)`
Fetch data from a Domo dataset or API endpoint.
**Parameters:**
- `url` (string) - API endpoint URL
- `options` (object, optional) - Request options
**Returns:** `Promise<ResponseBody>`
**Basic Usage:**
```javascript
// Returns array of objects by default
const data = await Domo.get('/data/v1/sales');
console.log(data); // [{ id: 1, amount: 100, ... }, ...]
```
**Format Options:**
```javascript
// CSV format
const csv = await Domo.get('/data/v1/sales', { format: 'csv' });
// Array of arrays with metadata
const arrayData = await Domo.get('/data/v1/sales', { format: 'array-of-arrays' });
console.log(arrayData.columns); // ['id', 'amount', 'date']
console.log(arrayData.rows); // [[1, 100, '2024-01-01'], ...]
// Excel format (returns Blob)
const excel = await Domo.get('/data/v1/sales', { format: 'excel' });
```
**Supported Formats:**
- `'array-of-objects'` (default) - Returns `ObjectResponseBody[]`
- `'array-of-arrays'` - Returns `ArrayResponseBody` with metadata
- `'csv'` - Returns CSV string
- `'excel'` - Returns Excel Blob
- `'plain'` - Returns plain text string
**Query Parameters:**
```javascript
const data = await Domo.get('/data/v1/sales', {
query: {
limit: 100,
offset: 0,
fields: 'id,amount,date'
}
});
```
**Best Practice:** Always filter and paginate large datasets to avoid slow responses:
```javascript
const data = await Domo.get('/data/v1/sales', {
query: {
limit: 1000,
offset: 0,
filter: 'date > "2024-01-01"'
}
});
```
---
#### `Domo.getAll(urls, options?)`
Fetch multiple datasets or endpoints in parallel.
**Parameters:**
- `urls` (string[]) - Array of API endpoint URLs
- `options` (object, optional) - Request options applied to all requests
**Returns:** `Promise<ResponseBody[]>`
**Example:**
```javascript
const [sales, inventory, customers] = await Domo.getAll([
'/data/v1/sales',
'/data/v1/inventory',
'/data/v1/customers'
]);
console.log(sales); // First dataset
console.log(inventory); // Second dataset
console.log(customers); // Third dataset
```
**With Options:**
```javascript
const results = await Domo.getAll(
['/data/v1/sales', '/data/v1/inventory'],
{ format: 'csv' }
);
// All results will be CSV strings
```
---
#### `Domo.post(url, body?, options?)`
Send a POST request to create data.
**Parameters:**
- `url` (string) - API endpoint URL
- `body` (object | string, optional) - Request body
- `options` (object, optional) - Request options
**Returns:** `Promise<ResponseBody>`
**Example:**
```javascript
// Create a document in Domo DataStore
const result = await Domo.post(
'/domo/datastores/v1/collections/users/documents/',
{
name: 'John Doe',
email: 'john@example.com',
role: 'Admin'
}
);
console.log(result); // Created document with ID
```
---
#### `Domo.put(url, body?, options?)`
Send a PUT request to update data.
**Parameters:**
- `url` (string) - API endpoint URL
- `body` (object | string, optional) - Request body
- `options` (object, optional) - Request options
**Returns:** `Promise<ResponseBody>`
**Example:**
```javascript
// Update an existing document
const result = await Domo.put(
'/domo/datastores/v1/collections/users/documents/abc123',
{
name: 'Jane Doe',
role: 'Manager'
}
);
```
---
#### `Domo.delete(url, options?)`
Send a DELETE request to remove data.
**Parameters:**
- `url` (string) - API endpoint URL
- `options` (object, optional) - Request options
**Returns:** `Promise<ResponseBody>`
**Example:**
```javascript
// Delete a document
await Domo.delete('/domo/datastores/v1/collections/users/documents/abc123');
```
---
#### `Domo.domoHttp(method, url, options?, body?)`
Low-level HTTP method for full control over requests. All other HTTP methods use this internally.
**Parameters:**
- `method` (RequestMethods) - HTTP method
- `url` (string) - API endpoint URL
- `options` (object, optional) - Request options
- `body` (object | string, optional) - Request body
**Returns:** `Promise<ResponseBody>`
**Example:**
```javascript
import { RequestMethods } from 'ryuu.js';
const result = await Domo.domoHttp(
RequestMethods.PATCH,
'/custom/endpoint',
{ format: 'array-of-objects' },
{ data: 'custom body' }
);
```
---
### Event Listeners
Event listeners enable real-time reactivity to changes in the Domo platform. All listener methods return an **unsubscribe function**.
#### `Domo.onDataUpdated(callback)`
Listen for dataset update events.
**Parameters:**
- `callback` (function) - Called when a dataset is updated
- Receives: `datasetAlias` (string) - Alias of the updated dataset
**Returns:** `function` - Unsubscribe function
**Example:**
```javascript
const unsubscribe = Domo.onDataUpdated((datasetAlias) => {
console.log(`Dataset ${datasetAlias} was updated`);
// Reload data for this dataset
loadData();
});
// Later, stop listening
unsubscribe();
```
**Use Case:** Refresh your app's visualizations when underlying data changes without requiring a page reload.
---
#### `Domo.onFiltersUpdated(callback)`
Listen for page filter changes.
**Parameters:**
- `callback` (function) - Called when filters change
- Receives: `filters` (Filter[]) - Array of filter objects
**Returns:** `function` - Unsubscribe function
**Example:**
```javascript
Domo.onFiltersUpdated((filters) => {
console.log('Filters updated:', filters);
// Find specific filter
const categoryFilter = filters.find(f => f.column === 'category');
if (categoryFilter) {
console.log('Category filter:', categoryFilter.values);
// Apply filter to your visualization
applyFilters(categoryFilter.values);
}
});
```
**Filter Object Structure:**
```javascript
{
column: "category", // Column name being filtered
operator: "IN", // Filter operator
values: ["ALERT", "WARNING"], // Array of filter values
dataType: "STRING", // Data type: STRING, NUMERIC, DATE, DATETIME
dataSourceId: "46d91556-...", // Source dataset ID (optional)
label: "category" // Display label (optional)
}
```
**Supported Operators:**
**String Operators:**
- `"IN"` - Value is in list
- `"NOT_IN"` - Value is not in list
- `"CONTAINS"` - Value contains string
- `"NOT_CONTAINS"` - Value doesn't contain string
- `"STARTS_WITH"` - Value starts with string
- `"NOT_STARTS_WITH"` - Value doesn't start with string
- `"ENDS_WITH"` - Value ends with string
- `"NOT_ENDS_WITH"` - Value doesn't end with string
**Numeric/Date Operators:**
- `"EQUALS"` - Equals value
- `"NOT_EQUALS"` - Not equals value
- `"GREATER_THAN"` - Greater than value
- `"GREAT_THAN_EQUALS_TO"` - Greater than or equals value
- `"LESS_THAN"` - Less than value
- `"LESS_THAN_EQUALS_TO"` - Less than or equals value
- `"BETWEEN"` - Between two values
---
#### `Domo.onVariablesUpdated(callback)`
Listen for page variable changes.
**Parameters:**
- `callback` (function) - Called when variables change
- Receives: `variables` (object) - Variables object with IDs as keys
**Returns:** `function` - Unsubscribe function
**Example:**
```javascript
Domo.onVariablesUpdated((variables) => {
console.log('Variables updated:', variables);
// Access specific variable by ID
const themeVariable = variables['391'];
if (themeVariable) {
const theme = themeVariable.parsedExpression.value;
setTheme(theme);
}
});
```
**Variables Object Structure:**
```javascript
{
"391": {
"parsedExpression": {
"exprType": "NUMERIC_VALUE",
"value": "9"
}
},
"392": {
"parsedExpression": {
"exprType": "STRING_VALUE",
"value": "dark"
}
}
}
```
**Note:** Variable IDs (like "391") are defined by Domo. Inspect the variables object in your app to find the correct IDs.
---
#### `Domo.onAppDataUpdated(callback)`
Listen for custom app data updates.
**Parameters:**
- `callback` (function) - Called when app data is received
- Receives: `data` (any) - Custom data object
**Returns:** `function` - Unsubscribe function
**Example:**
```javascript
Domo.onAppDataUpdated((data) => {
console.log('Received app data:', data);
// Handle custom data from other apps
if (data.action === 'highlight') {
highlightRow(data.rowId);
}
});
```
**Use Case:** Enable communication between multiple custom apps on the same Domo page.
---
### Emitters
Emitters send messages to the parent Domo platform to trigger actions or update state.
#### `Domo.requestFiltersUpdate(filters, pageStateUpdate?, onAck?, onReply?)`
Update page-level filters programmatically.
**Parameters:**
- `filters` (Filter[]) - Array of filter objects
- `pageStateUpdate` (boolean, optional) - Optional boolean indicating if the page state should be updated by the filter. When false, on the card level filter state will be updated. (default: true)
- `onAck` (function, optional) - Called when request is acknowledged
- `onReply` (function, optional) - Called when request completes
- Receives: `response` (any) - Response data
**Returns:** `string` - Request ID for tracking
**Example:**
```javascript
// Basic usage
Domo.requestFiltersUpdate([
{
column: 'category',
operator: 'IN',
values: ['ALERT', 'WARNING'],
dataType: 'STRING'
},
{
column: 'amount',
operator: 'GREATER_THAN',
values: [1000],
dataType: 'NUMERIC'
}
]);
```
**With Callbacks:**
```javascript
const requestId = Domo.requestFiltersUpdate(
filters,
true,
() => console.log('Filter update acknowledged'),
(response) => console.log('Filter update completed:', response)
);
console.log('Request ID:', requestId);
```
**Filter Requirements:**
All filter objects must include:
- `column` (string, required) - Column name to filter on
- `operator` (string, required) - Filter operator (see supported operators above)
- `values` (array, required) - Values to filter by
- `dataType` (string, required) - Data type: `"STRING"`, `"NUMERIC"`, `"DATE"`, or `"DATETIME"`
---
#### `Domo.requestVariablesUpdate(variables, onAck?, onReply?)`
Update page variables programmatically.
**Parameters:**
- `variables` (Variable[]) - Array of variable objects
- `onAck` (function, optional) - Called when request is acknowledged
- `onReply` (function, optional) - Called when request completes
**Returns:** `string` - Request ID for tracking
**Example:**
```javascript
Domo.requestVariablesUpdate([
{
functionId: 123,
value: 100
},
{
functionId: 124,
value: 'dark'
}
]);
```
**Variable Object Structure:**
```javascript
{
functionId: number, // Variable function ID from Domo
value: any // New value for the variable
}
```
---
#### `Domo.requestAppDataUpdate(data, onAck?, onReply?)`
Send custom app data to other apps on the same page.
**Parameters:**
- `data` (any) - Custom data object
- `onAck` (function, optional) - Called when request is acknowledged
- `onReply` (function, optional) - Called when request completes
**Returns:** `void`
**Example:**
```javascript
// Send custom data
Domo.requestAppDataUpdate({
action: 'highlight',
rowId: 123,
timestamp: Date.now()
});
```
**Use Case:** Coordinate interactions between multiple custom apps on the same Domo page.
---
### Navigation
#### `Domo.navigate(url, isNewWindow?)`
Navigate to a different page within Domo.
**Parameters:**
- `url` (string) - Domo page URL or route
- `isNewWindow` (boolean, optional) - Open in new tab/window (default: false)
**Example:**
```javascript
// Navigate to a profile page
Domo.navigate('/profile/3234');
// Open in new tab
Domo.navigate('/page/123456789', true);
```
**Important Notes:**
- Use `Domo.navigate()` instead of HTML links to change the page hosting the custom app
- For mobile web platforms, routes are automatically prefixed with `/m#` (e.g., `/m#/profile/3234`)
- External links are restricted to whitelisted domains (configure in Admin > Network Security > Custom Apps authorized domains)
---
### Environment
#### `Domo.env`
Access environment information about the current context.
**Type:** `QueryParams` (object)
**Available Properties:**
```javascript
Domo.env.pageId // Current page ID
Domo.env.userId // Current user ID
Domo.env.customer // Customer name
Domo.env.locale // Locale (e.g., 'en-US')
Domo.env.environment // Environment (e.g., 'dev3', 'prod')
Domo.env.platform // Platform (e.g., 'desktop', 'mobile')
```
**Example:**
```javascript
console.log(`User ${Domo.env.userId} on ${Domo.env.platform}`);
// Conditional logic based on platform
if (Domo.env.platform === 'mobile') {
renderMobileLayout();
} else {
renderDesktopLayout();
}
```
**Security Warning:** These properties come from URL query parameters and can be spoofed. For secure user identification, always verify with the API:
```javascript
const authenticatedUser = await Domo.get('/domo/environment/v1/');
console.log('Verified user:', authenticatedUser);
```
---
### Utilities
#### `Domo.extend(overrides)`
Extend or override static methods and properties of the Domo class.
**Parameters:**
- `overrides` (object) - Object with methods/properties to override
**Returns:** `void`
**Example:**
```javascript
import Domo, { get as originalGet } from 'ryuu.js';
// Add logging to all GET requests
Domo.extend({
get: async function(url, options) {
console.log(`[API] Fetching: ${url}`);
const startTime = Date.now();
try {
const result = await originalGet.call(this, url, options);
console.log(`[API] Success: ${url} (${Date.now() - startTime}ms)`);
return result;
} catch (error) {
console.error(`[API] Error: ${url} (${Date.now() - startTime}ms)`, error);
throw error;
}
}
});
// Now all Domo.get() calls include logging
const data = await Domo.get('/data/v1/sales');
```
**Use Cases:**
- Add logging to all requests
- Implement retry logic
- Add caching layer
- Mock responses for testing
- Add custom error handling
---
#### `Domo.getRequests()`
Get all tracked requests (ASK-ACK-REPLY pattern).
**Returns:** `AskReplyMap` - Object with request IDs as keys
**Example:**
```javascript
const requestId = Domo.requestFiltersUpdate(filters);
// Check request status
const requests = Domo.getRequests();
console.log(requests[requestId]);
// {
// request: { status: 'SENT', timestamp: 1234567890 },
// response: { status: 'SUCCESS', timestamp: 1234567900, data: {...} }
// }
```
---
#### `Domo.getRequest(id)`
Get a specific tracked request by ID.
**Parameters:**
- `id` (string) - Request ID
**Returns:** Request object or `undefined`
**Example:**
```javascript
const requestId = Domo.requestFiltersUpdate(filters);
const request = Domo.getRequest(requestId);
console.log(request.request.status); // 'PENDING', 'SENT'
```
---
#### `Domo.__util`
Internal utilities exposed for advanced use cases.
**Available Utilities:**
```javascript
Domo.__util.isSuccess(statusCode) // Check if HTTP status is success
Domo.__util.isVerifiedOrigin(origin) // Verify origin is trusted Domo domain
Domo.__util.getQueryParams() // Get current query parameters
Domo.__util.setFormatHeaders(format, headers) // Set Accept headers
Domo.__util.generateUniqueId() // Generate unique request ID
Domo.__util.isIOS() // Check if running on iOS
```
**Note:** These are internal utilities and may change between versions. Use at your own risk.
---
## TypeScript Support
ryuu.js includes comprehensive TypeScript definitions for a better development experience.
### Importing Types
```typescript
import Domo, {
// Interfaces
Filter,
Variable,
RequestOptions,
ObjectResponseBody,
ArrayResponseBody,
QueryParams,
// Enums
DataFormats,
DomoDataTypes,
RequestMethods,
// Type Guards
isFilter,
isFilterArray,
isVariable,
isVariableArray
} from 'ryuu.js';
```
### Typed Requests
```typescript
// Type-safe request options
const options: RequestOptions<'array-of-objects'> = {
format: 'array-of-objects',
query: { limit: 100 }
};
// Return type is automatically inferred
const data: ObjectResponseBody[] = await Domo.get('/data/v1/sales', options);
// Format-specific types
const csv: string = await Domo.get('/data/v1/sales', { format: 'csv' });
const arrays: ArrayResponseBody = await Domo.get('/data/v1/sales', { format: 'array-of-arrays' });
const excel: Blob = await Domo.get('/data/v1/sales', { format: 'excel' });
```
### Typed Filters
```typescript
const filters: Filter[] = [
{
column: 'category',
operator: 'IN',
values: ['ALERT', 'WARNING'],
dataType: 'STRING'
},
{
column: 'amount',
operator: 'GREATER_THAN',
values: [1000],
dataType: 'NUMERIC'
}
];
Domo.requestFiltersUpdate(filters);
```
### Typed Variables
```typescript
const variables: Variable[] = [
{ functionId: 123, value: 100 },
{ functionId: 124, value: 'dark' }
];
Domo.requestVariablesUpdate(variables);
```
### Custom Types
```typescript
// Define your data shape
interface SalesRecord {
id: number;
amount: number;
date: string;
category: string;
}
// Type-safe data access
const sales = await Domo.get('/data/v1/sales') as SalesRecord[];
sales.forEach(record => {
console.log(`Sale ${record.id}: $${record.amount}`);
});
```
---
## Mobile Platform Support
ryuu.js provides full support for iOS and Android mobile platforms through platform-specific APIs.
### iOS Integration
On iOS, ryuu.js uses **webkit.messageHandlers** for native communication:
```javascript
// Automatically handled by ryuu.js
Domo.requestFiltersUpdate(filters);
// Internally uses: webkit.messageHandlers.domofilter.postMessage()
Domo.requestVariablesUpdate(variables);
// Internally uses: webkit.messageHandlers.domovariable.postMessage()
```
### Android/Flutter Integration
On Android, ryuu.js uses global objects injected by the native app:
```javascript
// Automatically handled by ryuu.js
Domo.requestVariablesUpdate(variables);
// Internally uses: window.domovariable.postMessage()
Domo.requestFiltersUpdate(filters);
// Internally uses: window.domofilter.postMessage()
```
### Platform Detection
ryuu.js automatically detects the platform and uses the appropriate communication method:
```javascript
// Check if running on mobile
if (Domo.env.platform === 'mobile') {
// Mobile-specific logic
renderMobileView();
} else {
// Desktop-specific logic
renderDesktopView();
}
// Internal iOS detection (exposed via __util)
if (Domo.__util.isIOS()) {
// iOS-specific logic
}
```
### Mobile Considerations
1. **Navigation:** Routes are automatically prefixed with `/m#` on mobile web
2. **Touch Events:** Consider touch-friendly UI for mobile apps
3. **Performance:** Mobile devices may have limited resources - optimize data fetching
4. **Testing:** Test on actual devices or simulators for best results
---
## Error Handling
All HTTP methods return Promises. Use `try/catch` with async/await for error handling.
### Basic Error Handling
```javascript
try {
const data = await Domo.get('/data/v1/sales');
console.log('Data loaded:', data);
} catch (error) {
console.error('Failed to load data:', error);
}
```
### Error Object Structure
Error objects include comprehensive information:
```javascript
try {
const data = await Domo.get('/data/v1/nonexistent');
} catch (error) {
console.error('Status:', error.status); // 404
console.error('Status Text:', error.statusText); // 'Not Found'
console.error('Message:', error.message); // Error description
console.error('Body:', error.body); // Response body
console.error('Headers:', error.headers); // Response headers
}
```
### Handling Specific Error Types
```javascript
try {
const data = await Domo.get('/data/v1/sales');
} catch (error) {
if (error.status === 404) {
console.error('Dataset not found');
showError('The requested dataset does not exist');
} else if (error.status === 403) {
console.error('Access denied');
showError('You do not have permission to access this dataset');
} else if (error.status === 401) {
console.error('Authentication failed');
showError('Your session has expired. Please refresh the page.');
} else if (error.status >= 500) {
console.error('Server error');
showError('A server error occurred. Please try again later.');
} else {
console.error('Unexpected error:', error);
showError('An unexpected error occurred');
}
}
```
### Retry Logic
Implement retry logic for transient errors:
```javascript
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await Domo.get(url, options);
} catch (error) {
if (i === maxRetries - 1) throw error;
// Only retry on server errors
if (error.status >= 500) {
console.log(`Retry ${i + 1}/${maxRetries}...`);
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
} else {
throw error;
}
}
}
}
// Usage
try {
const data = await fetchWithRetry('/data/v1/sales');
} catch (error) {
console.error('Failed after retries:', error);
}
```
---
## Advanced Usage
### Custom Fetch Implementation
Provide your own fetch implementation for testing or custom behavior:
```javascript
// Mock fetch for testing
const mockFetch = async (url, options) => {
return {
ok: true,
status: 200,
headers: new Headers(),
json: async () => [{ id: 1, name: 'Test' }]
};
};
const data = await Domo.get('/data/v1/sales', {
fetchImpl: mockFetch
});
```
### Caching Layer
Add a caching layer using `extend()`:
```javascript
const cache = new Map();
Domo.extend({
get: async function(url, options) {
// Check cache
const cacheKey = `${url}:${JSON.stringify(options)}`;
if (cache.has(cacheKey)) {
console.log('Cache hit:', url);
return cache.get(cacheKey);
}
// Fetch and cache
const result = await originalGet.call(this, url, options);
cache.set(cacheKey, result);
return result;
}
});
```
### Request Interceptors
Add interceptors for all requests:
```javascript
import Domo, { domoHttp as originalDomoHttp } from 'ryuu.js';
Domo.extend({
domoHttp: async function(method, url, options, body) {
// Before request
console.log(`[${method}] ${url}`);
const startTime = Date.now();
// Add custom headers
options = {
...options,
headers: {
...options?.headers,
'X-Custom-Header': 'value'
}
};
try {
// Make request
const result = await originalDomoHttp.call(this, method, url, options, body);
// After successful request
console.log(`[${method}] ${url} - Success (${Date.now() - startTime}ms)`);
return result;
} catch (error) {
// After failed request
console.error(`[${method}] ${url} - Error (${Date.now() - startTime}ms)`, error);
throw error;
}
}
});
```
### Type Guards
Use type guards to validate runtime data:
```javascript
import { isFilter, isFilterArray, guardAgainstInvalidFilters } from 'ryuu.js';
// Validate individual filter
if (isFilter(data)) {
// TypeScript knows data is a Filter
console.log(data.column);
}
// Validate array of filters
if (isFilterArray(data)) {
// TypeScript knows data is Filter[]
data.forEach(filter => console.log(filter.column));
}
// Throw error if invalid
try {
guardAgainstInvalidFilters(data);
// Data is valid, proceed
} catch (error) {
console.error('Invalid filters:', error.message);
}
```
---
## Complete Example
Here's a comprehensive example showing a real-world custom app:
```javascript
import Domo from 'ryuu.js';
class SalesDashboard {
constructor() {
this.data = [];
this.filters = [];
this.initialize();
}
async initialize() {
// Set up event listeners
this.setupEventListeners();
// Load initial data
await this.loadData();
// Render dashboard
this.render();
}
setupEventListeners() {
// Listen for dataset updates
Domo.onDataUpdated((datasetAlias) => {
console.log(`Dataset ${datasetAlias} updated`);
this.loadData();
});
// Listen for filter changes
Domo.onFiltersUpdated((filters) => {
console.log('Filters updated:', filters);
this.filters = filters;
this.applyFilters();
});
// Listen for variable changes
Domo.onVariablesUpdated((variables) => {
console.log('Variables updated:', variables);
this.updateTheme(variables);
});
// Set up UI event handlers
document.getElementById('refreshBtn').addEventListener('click', () => {
this.loadData();
});
document.getElementById('exportBtn').addEventListener('click', () => {
this.exportData();
});
document.getElementById('filterBtn').addEventListener('click', () => {
this.updateFilters();
});
}
async loadData() {
try {
// Load multiple datasets in parallel
const [sales, customers, products] = await Domo.getAll([
'/data/v1/sales',
'/data/v1/customers',
'/data/v1/products'
]);
this.data = {
sales,
customers,
products
};
this.render();
} catch (error) {
console.error('Failed to load data:', error);
this.showError('Unable to load dashboard data');
}
}
applyFilters() {
// Find category filter
const categoryFilter = this.filters.find(f => f.column === 'category');
if (categoryFilter && categoryFilter.values.length > 0) {
// Filter local data
const filteredSales = this.data.sales.filter(sale =>
categoryFilter.values.includes(sale.category)
);
this.renderSales(filteredSales);
} else {
this.renderSales(this.data.sales);
}
}
updateFilters() {
// Get selected categories from UI
const selectedCategories = Array.from(
document.querySelectorAll('.category-checkbox:checked')
).map(cb => cb.value);
// Update page filters
Domo.requestFiltersUpdate(
[
{
column: 'category',
operator: 'IN',
values: selectedCategories,
dataType: 'STRING'
}
],
true,
() => console.log('Filter update acknowledged'),
(response) => console.log('Filter update completed:', response)
);
}
async exportData() {
try {
// Export as CSV
const csv = await Domo.get('/data/v1/sales', { format: 'csv' });
// Download file
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `sales-export-${Date.now()}.csv`;
a.click();
URL.revokeObjectURL(url);
console.log('Export successful');
} catch (error) {
console.error('Export failed:', error);
this.showError('Failed to export data');
}
}
updateTheme(variables) {
// Check for theme variable (example ID: 391)
const themeVar = variables['391'];
if (themeVar) {
const theme = themeVar.parsedExpression.value;
document.body.className = `theme-${theme}`;
}
}
render() {
if (!this.data.sales) return;
// Render sales chart
this.renderSales(this.data.sales);
// Render customer stats
this.renderCustomerStats(this.data.customers);
// Render product list
this.renderProducts(this.data.products);
}
renderSales(sales) {
const salesContainer = document.getElementById('salesChart');
// Calculate totals
const total = sales.reduce((sum, sale) => sum + sale.amount, 0);
const count = sales.length;
const average = total / count;
salesContainer.innerHTML = `
<div class="stats">
<div class="stat">
<h3>Total Sales</h3>
<p>$${total.toLocaleString()}</p>
</div>
<div class="stat">
<h3>Number of Sales</h3>
<p>${count.toLocaleString()}</p>
</div>
<div class="stat">
<h3>Average Sale</h3>
<p>$${average.toFixed(2)}</p>
</div>
</div>
`;
// Render chart (using your charting library)
// this.renderChart(sales);
}
renderCustomerStats(customers) {
const customerContainer = document.getElementById('customerStats');
customerContainer.innerHTML = `
<h3>Total Customers: ${customers.length}</h3>
<ul>
${customers.slice(0, 10).map(c => `
<li>${c.name} - ${c.email}</li>
`).join('')}
</ul>
`;
}
renderProducts(products) {
const productContainer = document.getElementById('productList');
productContainer.innerHTML = `
<table>
<thead>
<tr>
<th>Product</th>
<th>Price</th>
<th>Stock</th>
</tr>
</thead>
<tbody>
${products.map(p => `
<tr>
<td>${p.name}</td>
<td>$${p.price}</td>
<td>${p.stock}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
showError(message) {
const errorContainer = document.getElementById('error');
errorContainer.textContent = message;
errorContainer.style.display = 'block';
setTimeout(() => {
errorContainer.style.display = 'none';
}, 5000);
}
}
// Initialize app
new SalesDashboard();
```
---
## Migration Guide
### Deprecated Methods
The following methods have been renamed for consistency. Old methods still work but are deprecated and will be removed in a future version.
| Deprecated Method | New Method | Description |
|------------------|------------|-------------|
| `Domo.onDataUpdate` | `Domo.onDataUpdated` | Listen for dataset changes |
| `Domo.onFiltersUpdate` | `Domo.onFiltersUpdated` | Listen for filter changes |
| `Domo.onAppData` | `Domo.onAppDataUpdated` | Listen for app data changes |
| `Domo.filterContainer` | `Domo.requestFiltersUpdate` | Set page filters |
| `Domo.sendVariables` | `Domo.requestVariablesUpdate` | Update page variables |
| `Domo.sendAppData` | `Domo.requestAppDataUpdate` | Send custom app data |
### Migration Steps
1. **Find and Replace:**
```javascript
// Old
Domo.onDataUpdate((alias) => { ... });
Domo.onFiltersUpdate((filters) => { ... });
Domo.onAppData((data) => { ... });
Domo.filterContainer(filters);
Domo.sendVariables(variables);
Domo.sendAppData(data);
// New
Domo.onDataUpdated((alias) => { ... });
Domo.onFiltersUpdated((filters) => { ... });
Domo.onAppDataUpdated((data) => { ... });
Domo.requestFiltersUpdate(filters);
Domo.requestVariablesUpdate(variables);
Domo.requestAppDataUpdate(data);
```
2. **Update Dependencies:**
```bash
npm update ryuu.js
```
3. **Test Thoroughly:**
After migration, test all functionality to ensure everything works correctly.
---
## Contributing
Contributions are welcome! Please follow these guidelines:
1. **Fork the repository**
2. **Create a feature branch:** `git checkout -b feature/my-feature`
3. **Make your changes**
4. **Add tests** for new functionality
5. **Run tests:** `npm test`
6. **Build:** `npm run build`
7. **Commit:** `git commit -m "Add my feature"`
8. **Push:** `git push origin feature/my-feature`
9. **Open a Pull Request**
### Development Setup
```bash
# Clone repository
git clone https://github.com/your-org/domo.js.git
cd domo.js
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage
npm run coverage
# Build
npm run build
# Start demo server
npm run demo
```
---
## License
SEE LICENSE IN [LICENSE](./LICENSE)
Copyright (c) Domo
---
## Support
For issues, questions, or contributions:
- **Issues:** Open an issue on GitHub
- **Documentation:** See [CLAUDE.md](CLAUDE.md) for developer documentation
- **Domo Developer Portal:** Visit the Domo developer documentation
---
## Changelog
See [CHANGELOG.md](CHANGELOG.md) for version history and changes.
---
## v4 and Earlier Documentation
> **Note:** This documentation is for ryuu.js v4.x and earlier. For the latest version, see [v5+ Documentation](#v5-documentation).
## Table of Contents
- [Quick Start](#quick-start-1)
- [Features](#features-1)
- [Installation](#installation-1)
- [Core Concepts](#core-concepts-1)
- [API Reference](#api-reference-1)
- [HTTP Methods](#http-methods-1)
- [Event Listeners](#event-listeners-1)
- [Emitters](#emitters-1)
- [Navigation](#navigation-1)
- [Environment](#environment-1)
- [TypeScript Support](#typescript-support-1)
- [Mobile Platform Support](#mobile-platform-support-1)
- [Error Handling](#error-handling-1)
- [Complete Example](#complete-example-1)
- [Upgrading to v5+](#upgrading-to-v5)
- [Contributing](#contributing-1)
- [License](#license-1)
---
## Quick Start
Get started with ryuu.js in just a few lines:
```javascript
import domo from 'ryuu.js';
// Fetch data from a dataset
const data = await domo.get('/data/v1/sales');
console.log(data); // Array of objects with your data
// Listen for dataset updates
domo.onDataUpdate((alias) => {
console.log(`Dataset ${alias} was updated!`);
// Refresh your visualization
});
// Listen for filter changes
domo.onFiltersUpdate((filters) => {
console.log('Filters changed:', filters);
// Apply filters to your data
});
```
---
## Features
- **HTTP API Access** - Authenticated requests to Domo datasets, datastores, and APIs
- **Real-time Events** - Listen for dataset updates, filter changes, and variable updates
- **Filter Management** - Get and set page-level filters programmatically
- **Variable Management** - Access and update page variables
- **Custom App Data** - Send custom data between apps on the same page
- **Mobile Support** - iOS and Android compatibility
- **TypeScript Ready** - Type definitions included
- **Zero Dependencies** - Minimal bundle size
---
## Installation
### NPM
```bash
npm install ryuu.js@4
```
Then import in your application:
```javascript
// ES modules
import domo from 'ryuu.js';
// CommonJS
const domo = require('ryuu.js');
```
### CDN / Script Tag
```html
<script src="https://unpkg.com/ryuu.js@4"></script>
<script>
// domo is available globally
domo.get('/data/v1/sales').then(data => {
console.log(data);
});
</script>
```
### TypeScript
TypeScript definitions are included automatically:
```typescript
import domo, { Filter, Variable, RequestOptions } from 'ryuu.js';
```
---
## Core Concepts
### Communication Architecture
Your custom app runs in an iframe within the Domo platform. ryuu.js v4 establishes bidirectional communication using:
- **window.postMessage API** - Primary communication mechanism for desktop/web
- **webkit.messageHandlers** - iOS native integration
- **Global objects** - Android integration
### Environment Context
Access information about the current user and environment via `domo.env`:
```javascript
console.log(domo.env.userId); // Current user ID
console.log(domo.env.customer); // Customer name
console.log(domo.env.pageId); // Current page ID
console.log(domo.env.locale); // Locale (e.g., 'en-US')
console.log(domo.env.platform); // Platform (e.g., 'desktop', 'mobile')
```
**Security Note:** Environment properties come from URL parameters and can be spoofed. Always verify with the API for secure operations:
```javascript
const authenticatedUser = await domo.get('/domo/environment/v1/');
```
---
## API Reference
### HTTP Methods
All HTTP methods return Promises and support multiple data formats.
#### `domo.get(url, options?)`
Fetch data from a Domo dataset or API endpoint.
**Parameters:**
- `url` (string) - API endpoint URL
- `options` (object, optional) - Request options
**Returns:** `Promise<ResponseBody>`
**Basic Usage:**
```javascript
// Returns array of objects by default
const data = await domo.get('/data/v1/sales');
console.log(data); // [{ id: 1, amount: 100, ... }, ...]
```
**Format Options:**
```javascript
// CSV format
const csv = await domo.get('/data/v1/sales', { format: 'csv' });
// Array of arrays with metadata
const arrayData = await domo.get('/data/v1/sales', { format: 'array-of-arrays' });
console.log(arrayData.columns); // ['id', 'amount', 'date']
console.log(arrayData.rows); // [[1, 100, '2024-01-01'], ...]
// Excel format (returns Blob)
const excel = await domo.get('/data/v1/sales', { format: 'excel' });
```
**Supported Formats:**
- `'array-of-objects'` (default) - Returns `ObjectResponseBody[]`
- `'array-of-arrays'` - Returns `ArrayResponseBody` with metadata
- `'csv'` - Returns CSV string
- `'excel'` - Returns Excel Blob
- `'plain'` - Returns plain text string
**Query Parameters:**
```javascript
const data = await domo.get('/data/v1/sales', {
query: {
limit: 100,
offset: 0,
fields: 'id,amount,date'
}
});
```
---
#### `domo.getAll(urls, options?)`
Fetch multiple datasets or endpoints in parallel.
**Parameters:**
- `urls` (string[]) - Array of API endpoint URLs
- `options` (object, optional) - Request options applied to all requests
**Returns:** `Promise<ResponseBody[]>`
**Example:**
```javascript
const [sales, inventory, customers] = await domo.getAll([
'/data/v1/sales',
'/data/v1/inventory',
'/data/v1/customers'
]);
```
---
#### `domo.post(url, body?, options?)`
Send a POST request to create data.
**Parameters:**
- `url` (string) - API endpoint URL
- `body` (object | string, optional) - Request body
- `options` (object, optional) - Request options
**Returns:** `Promise<ResponseBody>`
**Example:**
```javascript
// Create a document in Domo DataStore
const result = await domo.post(
'/domo/datastores/v1/collections/users/documents/',
{
name: 'John Doe',
email: 'john@example.com',
role: 'Admin'
}
);
```
---
#### `domo.put(url, body?, options?)`
Send a PUT request to update data.
**Example:**
```javascript
// Update an existing document
const result = await domo.put(
'/domo/datastores/v1/collections/users/documents/abc123',
{
name: 'Jane Doe',
role: 'Manager'
}
);
```
---
#### `domo.delete(url, options?)`
Send a DELETE request to remove data.
**Example:**
```javascript
// Delete a document
await domo.delete('/domo/datastores/v1/collections/users/documents/abc123');
```
---
### Event Listeners
Event listeners enable real-time reactivity to changes in the Domo platform. All listener methods return an **unsubscribe function**.
#### `domo.onDataUpdate(callback)`
Listen for dataset update events.
**Parameters:**
- `callback` (function) - Called when a dataset is updated
- Receives: `datasetAlias` (string) - Alias of the updated dataset
**Returns:** `function` - Unsubscribe function
**Example:**
```javascript
const unsubscribe = domo.onDataUpdate((datasetAlias) => {
console.log(`Dataset ${datasetAlias} was updated`);
// Reload data for this dataset
loadData();
});
// Later, stop listening
unsubscribe();
```
---
#### `domo.onFiltersUpdate(callback)`
Listen for page filter changes.
**Parameters:**
- `callback` (function) - Called when filters change
- Receives: `filters` (Filter[]) - Array of filter objects
**Returns:** `function` - Unsubscribe function
**Example:**
```javascript
domo.onFiltersUpdate((filters) => {
console.log('Filters updated:', filters);
// Find specific filter
const categoryFilter = filters.find(f => f.column === 'category');
if (categoryFilter) {
console.log('Category filter:', categoryFilter.values);
// Apply filter to your visualization
applyFilters(categoryFilter.values);
}
});
```
**Filter Object Structure:**
```javascript
{
column: "category", // Column name being filtered
operator: "IN", // Filter operator
values: ["ALERT", "WARNING"], // Array of filter values
dataType: "STRING", // Data type: STRING, NUMERIC, DATE, DATETIME
dataSourceId: "46d91556-...", // Source dataset ID (optional)
label: "category" // Display label (optional)
}
```
**Supported Operators:**
**String Operators:**
- `"IN"` / `"NOT_IN"`
- `"CONTAINS"` / `"NOT_CONTAINS"`
- `"STARTS_WITH"` / `"NOT_STARTS_WITH"`
- `"ENDS_WITH"` / `"NOT_ENDS_WITH"`
**Numeric/Date Operators:**
- `"EQUALS"` / `"NOT_EQUALS"`
- `"GREATER_THAN"` / `"GREAT_THAN_EQUALS_TO"`
- `"LESS_THAN"` / `"LESS_THAN_EQUALS_TO"`
- `"BETWEEN"`
---
#### `domo.onAppData(callback)`
Listen for custom app data updates.
**Parameters:**
- `callback` (function) - Called when app data is received
- Receives: `data` (any) - Custom data object
**Returns:** `function` - Unsubscribe function
**Example:**
```javascript
domo.onAppData((data) => {
console.log('Received app data:', data);
// Handle custom data from other apps
if (data.action === 'highlight') {
highlightRow(data.rowId);
}
});
```
---
### Emitters
Emitters send messages to the parent Domo platform to trigger actions or update state.
#### `domo.filterContainer(filters, pageStateUpdate?)`
Update page-level filters programmatically.
**Parameters:**
- `filters` (Filter[]) - Array of filter objects
- `pageStateUpdate` (boolean, optional) - Whether to update page state (default: true)
**Returns:** `void`
**Example:**
```javascript
// Basic usage
domo.filterContainer([
{
column: 'category',
operator: 'IN',
values: ['ALERT', 'WARNING'],
dataType: 'STRING'
},
{
column: 'amount',
operator: 'GREATER_THAN',
values: [1000],
dataType: 'NUMERIC'
}
]);
```
---
#### `domo.sendVariables(variables)`
Update page variables programmatically.
**Parameters:**
- `variables` (Variable[]) - Array of variable objects
**Returns:** `void`
**Example:**
```javascript
domo.sendVariables([
{
functionId: 123,
value: 100
},
{
functionId: 124,
value: 'dark'
}
]);
```
**Variable Object Structure:**
```javascript
{
functionId: number, // Variable function ID from Domo
value: any // New value for the variable
}
```
---
#### `domo.sendAppData(data)`
Send custom app data to other apps on the same page.
**Parameters:**
- `data` (any) - Custom data object
**Returns:** `void`
**Example:**
```javascript
// Send custom data
domo.sendAppData({
action: 'highlight',
rowId: 123,
timestamp: Date.now()
});
```
---
### Navigation
Navigation in v4 is available through the parent window. For programmatic navigation, you can use:
```javascript
// Navigate within Domo
window.parent.postMessage({
event: 'navigate',
data: { route: '/page/123456789' }
}, '*');
```
---
### Environment
#### `domo.env`
Access environment information about the current context.
**Type:** `QueryParams` (object)
**Available Properties:**
```javascript
domo.env.pageId // Current page ID
domo.env.userId // Current user ID
domo.env.customer // Customer name
domo.env.locale // Locale (e.g., 'en-US')
domo.env.environment // Environment (e.g., 'dev3', 'prod')
domo.env.platform // Platform (e.g., 'desktop', 'mobile')
```
---
## TypeScript Support
ryuu.js v4 includes TypeScript definitions.
### Importing Types
```typescript
import domo, {
// Interfaces
Filter,
Variable,
RequestOptions,
ObjectResponseBody,
ArrayResponseBody,
QueryParams,
// Enums
DataFormats,
DomoDataTypes,
RequestMethods
} from 'ryuu.js';
```
### Typed Requests
```typescript
// Type-safe request options
const options: RequestOptions<'array-of-objects'> = {
format: 'array-of-objects',
query: { limit: 100 }
};
// Return type is automatically inferred
const data: ObjectResponseBody[] = await domo.get('/data/v1/sales', options);
// Format-specific types
const csv: string = await domo.get('/data/v1/sales', { format: 'csv' });
const arrays: ArrayResponseBody = await domo.get('/data/v1/sales', { format: 'array-of-arrays' });
```
### Typed Filters
```typescript
const filters: Filter[] = [
{
column: 'category',
operator: 'IN',
values: ['ALERT', 'WARNING'],
dataType: 'STRING'
}
];
domo.filterContainer(filters);
```
---
## Mobile Platform Support
ryuu.js v4 provides support for iOS and Android mobile platforms.
### iOS Integration
On iOS, ryuu.js uses **webkit.messageHandlers** for native communication:
```javascript
// Automatically handled by ryuu.js
domo.filterContainer(filters);
// Internally uses: webkit.messageHandlers.domofilter.postMessage()
domo.sendVariables(variables);
// Internally uses: webkit.messageHandlers.domovariable.postMessage()
```
### Android Integration
On Android, ryuu.js uses global objects injected by the native app:
```javascript
// Automatically handled by ryuu.js
domo.sendVariables(variables);
// Internally uses: window.domovariable.postMessage()
domo.filterContainer(filters);
// Internally uses: