Tenants: Bulk Sync Connection Paths (JavaScript)
Tenants: Bulk Sync Connection Paths (JavaScript)
This script demonstrates how to perform bulk operations across all tenants for a parent Sigma organization using OAuth token exchange for secure tenant impersonation. It retrieves all tenants using pagination, obtains tenant-scoped tokens, gets their connections, and triggers a sync operation for a specified database path. This pattern can be adapted for other bulk tenant operations.
Sigma Tenants is a premium feature.
The script includes:
- OAuth token exchange for tenant-scoped authentication
- Token caching to minimize authentication requests
- Pagination handling for both tenants and connections
- Optional filtering by connection name
- Concurrency control to manage API rate limits
- Comprehensive error handling and reporting
- Performance metrics and summary statistics
Each section of the script has inline comments provided.
Refer to the Sigma REST API Recipes QuickStart for step-by-step instructions.
Example script
JavaScript
// This script will sync a specified path for all tenant connections, using pagination.// It finds all tenants, gets their connections, and triggers a sync for the specified path.// 1: Load environment variables from a specific .env file for configurationrequire('dotenv').config({ path: 'sigma-api-recipes/.env' });const crypto = require('crypto');const axios = require('axios');// 2: Load variables from environmentconst baseURL = process.env.baseURL;const clientId = process.env.CLIENT_ID;const clientSecret = process.env.CLIENT_SECRET;// 3: Configurationconst CONNECTION_NAME_FILTER = 'My Connection'; // Optional: filter by connection name (set to null to sync all connections)const CONCURRENCY = 3; // Number of parallel requests// Note that this can be used to sync any path, not just a table.// Different data platforms have different path length requirements (e.g., Redshift uses 2 levels, while Snowflake uses 3).const SYNC_PATH = ['MY_DATABASE', 'MY_SCHEMA', 'MY_TABLE']; // Path to sync (database, schema, table)// Optional: Token cache to avoid unnecessary token requestslet parentTokenCache = { token: '', expiresAt: 0 };const tenantTokenCache = new Map();function base64UrlEncode(data) {return Buffer.from(data).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');}// Get parent org access tokenasync function getParentToken() {if (parentTokenCache.token && Date.now() < parentTokenCache.expiresAt - 60000) {return parentTokenCache.token;}const params = new URLSearchParams({grant_type: 'client_credentials',client_id: clientId,client_secret: clientSecret,});const response = await axios.post(`${baseURL}/v2/auth/token`, params.toString(), {headers: { 'Content-Type': 'application/x-www-form-urlencoded' }});parentTokenCache = {token: response.data.access_token,expiresAt: Date.now() + response.data.expires_in * 1000};return parentTokenCache.token;}// Create self-signed JWT subject token for tenant impersonationfunction createSubjectToken(tenantOrganizationId) {const now = Math.floor(Date.now() / 1000);const header = { alg: 'HS256', typ: 'JWT', kid: clientId };const payload = { iat: now, exp: now + 60, tenant: tenantOrganizationId };const encodedHeader = base64UrlEncode(JSON.stringify(header));const encodedPayload = base64UrlEncode(JSON.stringify(payload));const signatureInput = `${encodedHeader}.${encodedPayload}`;const signature = crypto.createHmac('sha256', clientSecret).update(signatureInput).digest('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');return `${signatureInput}.${signature}`;}// Exchange parent token for tenant-scoped tokenasync function getTenantToken(tenantOrganizationId) {const cached = tenantTokenCache.get(tenantOrganizationId);if (cached && Date.now() < cached.expiresAt - 60000) {return cached.token;}const actorToken = await getParentToken();const subjectToken = createSubjectToken(tenantOrganizationId);const params = new URLSearchParams({grant_type: 'urn:ietf:params:oauth:grant-type:token-exchange',subject_token: subjectToken,subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',actor_token: actorToken,actor_token_type: 'urn:ietf:params:oauth:token-type:access_token',});const response = await axios.post(`${baseURL}/v2/auth/token`, params.toString(), {headers: {'Content-Type': 'application/x-www-form-urlencoded',Authorization: `Bearer ${actorToken}`}});tenantTokenCache.set(tenantOrganizationId, {token: response.data.access_token,expiresAt: Date.now() + response.data.expires_in * 1000});return response.data.access_token;}// Get all tenants using paginationasync function getAllTenants() {const token = await getParentToken();let nextPageToken = null;const allTenants = [];do {const url = nextPageToken? `${baseURL}/v2/tenants?pageToken=${nextPageToken}`: `${baseURL}/v2/tenants`;const response = await axios.get(url, {headers: { Authorization: `Bearer ${token}` }});allTenants.push(...response.data.entries.filter(e => e !== null));nextPageToken = response.data.nextPageToken || null;} while (nextPageToken);console.log(`Found ${allTenants.length} tenants`);return allTenants;}// Get connections for a tenant (using tenant-scoped auth token)async function getTenantConnections(tenantOrganizationId) {const token = await getTenantToken(tenantOrganizationId);const allConnections = [];let nextPage = null;do {const params = new URLSearchParams({ limit: '100' });if (nextPage) params.set('page', nextPage);const response = await axios.get(`${baseURL}/v2/connections?${params}`, {headers: { Authorization: `Bearer ${token}` }});allConnections.push(...response.data.entries);nextPage = response.data.nextPage || null;} while (nextPage);return allConnections;}// Sync a connection by path (using tenant-scoped token)async function syncConnection(tenantOrganizationId, connectionId, path) {const token = await getTenantToken(tenantOrganizationId);const startTime = Date.now();await axios.post(`${baseURL}/v2/connections/${connectionId}/sync`,{ path },{ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } });return { latencyMs: Date.now() - startTime };}// Function to process items with controlled concurrencyasync function processWithConcurrency(items, concurrency, processor) {const results = [];const executing = new Set();for (const item of items) {const promise = processor(item).then(result => {executing.delete(promise);return result;});executing.add(promise);results.push(promise);if (executing.size >= concurrency) {await Promise.race(executing);}}return Promise.all(results);}// Main function to sync connection paths across all tenantsasync function syncTenantConnectionPaths() {const startTime = Date.now();// Step 1: Get all tenantsconsole.log('\n=== Fetching Tenants ===');const tenants = await getAllTenants();// Step 2: Process each tenantconsole.log('\n=== Syncing Connection Paths ===');const results = [];await processWithConcurrency(tenants, CONCURRENCY, async (tenant) => {try {// Get connections for this tenantconst connections = await getTenantConnections(tenant.organizationId);// Filter by connection name// Note: similar APIs could be used to retrieve connections via the source swap policyconst targetConnections = CONNECTION_NAME_FILTER? connections.filter(c => c.name === CONNECTION_NAME_FILTER): connections;if (targetConnections.length === 0) {results.push({tenantName: tenant.name,status: 'skipped',reason: CONNECTION_NAME_FILTER? `No connection named "${CONNECTION_NAME_FILTER}"`: 'No connections found'});return;}// Sync each matching connectionfor (const connection of targetConnections) {try {const syncResult = await syncConnection(tenant.organizationId,connection.connectionId,SYNC_PATH);results.push({tenantName: tenant.name,connectionName: connection.name,status: 'success',latencyMs: syncResult.latencyMs});console.log(`✓ ${tenant.name} - ${connection.name} synced in ${syncResult.latencyMs}ms`);} catch (error) {results.push({tenantName: tenant.name,connectionName: connection.name,status: 'failed',error: error.response?.data?.message || error.message});console.error(`✗ ${tenant.name} - ${connection.name}: ${error.message}`);}}} catch (error) {results.push({tenantName: tenant.name,status: 'failed',error: error.response?.data?.message || error.message});console.error(`✗ ${tenant.name}: ${error.message}`);}});// Step 3: Print summaryconst totalElapsedMs = Date.now() - startTime;const successes = results.filter(r => r.status === 'success');const failures = results.filter(r => r.status === 'failed');const skipped = results.filter(r => r.status === 'skipped');console.log('\n=== Summary ===');console.log(`Path synced: ${SYNC_PATH.join('.')}`);console.log(`Total tenants: ${tenants.length}`);console.log(`Successful syncs: ${successes.length}`);console.log(`Failed syncs: ${failures.length}`);console.log(`Skipped: ${skipped.length}`);console.log(`Total elapsed time: ${(totalElapsedMs / 1000).toFixed(2)}s`);if (successes.length > 0) {const latencies = successes.map(r => r.latencyMs).sort((a, b) => a - b);const avgLatency = latencies.reduce((a, b) => a + b, 0) / latencies.length;console.log(`\nLatency stats:`);console.log(` Min: ${latencies[0]}ms`);console.log(` Max: ${latencies[latencies.length - 1]}ms`);console.log(` Avg: ${avgLatency.toFixed(0)}ms`);}if (failures.length > 0) {console.log('\nFailed syncs:');console.table(failures.slice(0, 10).map(f => ({Tenant: f.tenantName,Connection: f.connectionName || 'N/A',Error: f.error})));}}// Execute the main functionsyncTenantConnectionPaths().catch(error => {console.error('Failed to sync connection paths:', error);});
Response Example
✓ Tenant A - My Connection synced in 245ms✓ Tenant B - My Connection synced in 198ms✓ Tenant C - My Connection synced in 312ms=== Summary ===Path synced: MY_DATABASE.MY_SCHEMA.MY_TABLETotal tenants: 3Successful syncs: 3Failed syncs: 0Skipped: 0Total elapsed time: 1.23sLatency stats:Min: 198msMax: 312msAvg: 252ms
Endpoints used
- Get parent organization access token:
POST ${baseURL}/v2/auth/token(client credentials grant) - Exchange for tenant-scoped token:
POST ${baseURL}/v2/auth/token(token exchange grant) - List all tenants:
GET ${baseURL}/v2/tenants - List connections:
GET ${baseURL}/v2/connections - Sync connection by path:
POST ${baseURL}/v2/connections/${connectionId}/sync

