UNPKG

ryuu.js

Version:

Ryuu JavaScript Utility Library

2,171 lines (1,611 loc) 59.2 kB
# ryuu.js > A powerful JavaScript SDK for building custom applications within the Domo platform [![npm version](https://img.shields.io/npm/v/ryuu.js.svg)](https://www.npmjs.com/package/ryuu.js) [![License](https://img.shields.io/badge/license-SEE%20LICENSE-blue.svg)](./LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](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: