Develop a JavaScript Query

This section of the crash course covers Deephaven Enterprise's client APIs. This guide walks you through a simple end-to-end task:

  • connecting to a Deephaven Enterprise server using JavaScript,
  • opening a specific table,
  • displaying its data,
  • and applying a basic filter.

Note

To run these examples in your environment, you'll need to replace the following placeholder values with your actual information:

  • https://myserver/ - Replace with your actual Deephaven server URL
  • Username and password in authentication examples
  • 'MyQuery' - Replace with an actual Persistent Query name available on your server
  • 'my_data_table' - Replace with an actual table name from your query

The complete example at the end of this guide contains all the necessary HTML and JavaScript code. Copy this example, make the replacements noted above, and save it as an HTML file to get started quickly.

Prerequisites

To get started, you need to load the Deephaven JS library. This file is typically loaded from the Deephaven server itself. It resides at /irisapi/irisapi.nocache.js on the web server. In this example, we'll assume your Deephaven server is hosted at https://myserver/.

Include this script tag in your HTML file:

<script
  src="https://myserver/irisapi/irisapi.nocache.js"
  type="text/javascript"
></script>

1. Connect and authenticate

First, create a dh.Client instance and log in. This example uses password authentication.

async function connectAndAuthenticate() {
  // Connect to the server (use wss for https servers)
  const client = new dh.Client('wss://myserver/socket');

  return new Promise((resolve, reject) => {
    client.addEventListener(dh.Client.EVENT_CONNECT, () => {
      // Login - replace with your actual username and password/token
      client
        .login({
          username: 'user',
          token: 'password',
          type: 'password',
        })
        .then(
          () => {
            console.log('Successfully logged in!');
            resolve(client);
          },
          (error) => {
            reject(new Error('Error initializing client', { cause: error }));
          }
        );
    });
  });
}

2. Open a Specific Table

Once you have an authenticated client, you can open tables from a Persistent Query using Core+ functionality.

async function openTable(client) {
  try {
    // Get information about the Persistent Query
    // Note: In a real app, you might fetch all known configs first
    // and let the user select, or have these names configurable.
    const configs = await client.getKnownConfigs();
    const queryInfo = configs.find((q) => q.name === 'MyQuery');
    if (!queryInfo) {
      throw new Error('Persistent Query "MyQuery" not found.');
    }

    console.log('Opening Core+ table...');
    // For Core+ queries, we need to use the Core+ API
    const table = await getCorePlusTable(client, queryInfo, 'my_data_table');
    console.log('Table "my_data_table" opened successfully.');
    return table;
  } catch (error) {
    throw new Error('Error opening or processing table', { cause: error });
  }
}

// Helper function to get a table from a Core+ query
async function getCorePlusTable(enterpriseClient, queryInfo, tableName) {
  // Dynamically import the API from the provided URL
  console.log('Importing Core+ API from:', queryInfo.jsApiUrl);
  try {
    const api = (await import(queryInfo.jsApiUrl)).default;
    console.log('Core+ API bootstrapped successfully');

    // Create an auth token for the Core+ client
    const token = await enterpriseClient.createAuthToken(
      'RemoteQueryProcessor'
    );

    // Create a Core+ client and connect
    const { grpcUrl, envoyPrefix } = queryInfo;
    const clientOptions = envoyPrefix
      ? {
          headers: { 'envoy-prefix': envoyPrefix },
        }
      : undefined;

    const corePlusClient = new api.CoreClient(grpcUrl, clientOptions);

    // Login to the Core+ client
    await corePlusClient.login({
      type: 'io.deephaven.proto.auth.Token',
      token,
    });
    console.log('Logged in to Core+ client');

    // Get the connection and table
    const connection = await corePlusClient.getAsIdeConnection();
    return connection.getObject({
      name: tableName,
      type: 'Table',
    });
  } catch (error) {
    throw new Error('Error getting Core+ table', { cause: error });
  }
}

This code uses the Core+ API to connect to the query and fetch the table. It dynamically imports the API from the URL provided in the query information, authenticates with a token, and then retrieves the table object.

3. Display Table Data

To display data from a table, you set a viewport and get its data.

async function displayTableData(table, title = 'Table Data') {
  const tableElement = document.getElementById('dataTable');
  tableElement.innerHTML = ''; // Clear previous content

  const caption = tableElement.createCaption();
  caption.textContent = title;

  const thead = tableElement.createTHead();
  const headerRow = thead.insertRow();
  table.columns.forEach((col) => {
    const th = document.createElement('th');
    th.textContent = col.name;
    headerRow.appendChild(th);
  });

  const tbody = tableElement.createTBody();

  // Set a viewport for the first 10 rows and get the subscription
  const subscription = table.setViewport(0, 9, table.columns);

  try {
    const viewportData = await subscription.getViewportData();
    viewportData.rows.forEach((dataRow) => {
      const tr = tbody.insertRow();
      table.columns.forEach((col) => {
        const td = tr.insertCell();
        td.textContent = dataRow.get(col);
      });
    });
    console.log(`Displayed ${viewportData.rows.length} rows.`);
  } finally {
    subscription.close(); // Ensure subscription is closed
  }
}

4. Apply a Simple Filter

async function applyFilterToTable(originalTable) {
  try {
    // Example: Filter for rows where a column 'Category' equals 'A'
    // First, find the 'Category' column object
    const categoryColumn = originalTable.columns.find(
      (col) => col.name === 'Category'
    );
    if (!categoryColumn) {
      throw new Error('Column "Category" not found for filtering.');
    }

    // Create a filter condition
    const filterCondition = categoryColumn.filter().eq('A');

    // Apply the filter. This creates a new view on the server.
    // For simplicity, we'll work with the same table object which updates its underlying view.
    await originalTable.applyFilter([filterCondition]);
    console.log('Filter applied.');

    // Re-display the table data with a new title
    await displayTableData(originalTable, 'Filtered Table Data (Category A)');

    // To clear filters:
    // await originalTable.applyFilter([]);
  } catch (error) {
    throw new Error('Error applying filter', { cause: error });
  }
}

5. Put it all together

async function main() {
  try {
    const client = await connectAndAuthenticate();
    const table = await openTable(client);

    // Display the unfiltered table
    await displayTableData(table, 'Unfiltered Table Data');

    // Apply a filter to the table (which also displays the filtered table)
    await applyFilterToTable(table);
  } catch (error) {
    console.error('Error occurred:', error);
  }
}

Complete example

Here's a full HTML page combining all the steps. Replace https://myserver/, user, password, MyQuery, my_data_table, and column names with your actual details.

<!doctype html>
<html>
  <head>
    <title>Deephaven JS Crash Course</title>
    <script
      src="https://myserver/irisapi/irisapi.nocache.js"
      type="text/javascript"
    ></script>
    <style>
      table {
        border-collapse: collapse;
        margin-top: 20px;
      }
      th,
      td {
        border: 1px solid black;
        padding: 8px;
        text-align: left;
      }
      caption {
        font-weight: bold;
        margin-bottom: 5px;
      }
    </style>
  </head>
  <body>
    <h1>Deephaven JS API - Basic Table Display</h1>
    <table id="dataTable"></table>

    <script>
      // Step 1: Connect and authenticate
      async function connectAndAuthenticate() {
        try {
          // Connect to the server (use wss for https servers)
          const client = new dh.Client('wss://myserver/socket');

          // Login - replace with your actual username and password/token
          await client.login({
            username: 'user',
            token: 'password',
            type: 'password',
          });
          console.log('Successfully logged in!');
          return client;
        } catch (error) {
          throw new Error('Error connecting or logging in', { cause: error });
        }
      }

      // Step 2: Open a Specific Table
      async function openTable(client) {
        try {
          // Get information about the Persistent Query
          const queryInfo = client
            .getKnownConfigs()
            .find((config) => config.name === 'MyQuery');
          if (!queryInfo) {
            throw new Error('Persistent Query "MyQuery" not found.');
          }

          console.log('Opening Core+ table...');
          // For Core+ queries, we need to use the Core+ API
          const table = await getCorePlusTable(
            client,
            queryInfo,
            'my_data_table'
          );
          console.log('Table "my_data_table" opened successfully.');
          return table;
        } catch (error) {
          throw new Error('Error opening or processing table', {
            cause: error,
          });
        }
      }

      // Helper function to get a table from a Core+ query
      async function getCorePlusTable(enterpriseClient, queryInfo, tableName) {
        try {
          // Dynamically import the API from the provided URL
          console.log('Importing Core+ API from:', queryInfo.jsApiUrl);
          const api = (await import(queryInfo.jsApiUrl)).default;
          console.log('Core+ API bootstrapped successfully');

          // Create an auth token for the Core+ client
          const token = await enterpriseClient.createAuthToken(
            'RemoteQueryProcessor'
          );

          // Create a Core+ client and connect
          const { grpcUrl, envoyPrefix } = queryInfo;
          const clientOptions = envoyPrefix
            ? {
                headers: { 'envoy-prefix': envoyPrefix },
              }
            : undefined;

          const corePlusClient = new api.CoreClient(grpcUrl, clientOptions);

          // Login to the Core+ client
          await corePlusClient.login({
            type: 'io.deephaven.proto.auth.Token',
            token,
          });

          // Get the connection and table
          const connection = await corePlusClient.getAsIdeConnection();
          return connection.getObject({
            name: tableName,
            type: 'Table',
          });
        } catch (error) {
          throw new Error('Error getting Core+ table', { cause: error });
        }
      }

      // Step 3: Display Table Data
      async function displayTableData(table, title = 'Table Data') {
        const tableElement = document.getElementById('dataTable');
        tableElement.innerHTML = ''; // Clear previous content

        const caption = tableElement.createCaption();
        caption.textContent = title;

        const thead = tableElement.createTHead();
        const headerRow = thead.insertRow();
        table.columns.forEach((col) => {
          const th = document.createElement('th');
          th.textContent = col.name;
          headerRow.appendChild(th);
        });

        const tbody = tableElement.createTBody();

        // Set a viewport for the first 10 rows and get the subscription
        const subscription = table.setViewport(0, 9, table.columns);

        // For this basic guide, we'll use getViewportData for a one-time fetch
        try {
          const viewportData = await subscription.getViewportData();
          viewportData.rows.forEach((dataRow) => {
            const tr = tbody.insertRow();
            table.columns.forEach((col) => {
              const td = tr.insertCell();
              td.textContent = dataRow.get(col);
            });
          });
          console.log(`Displayed ${viewportData.rows.length} rows.`);
        } finally {
          subscription.close(); // Ensure subscription is closed
        }
      }

      // Step 4: Apply a Simple Filter
      async function applyFilterToTable(originalTable) {
        try {
          // Example: Filter for rows where a column 'Category' equals 'A'
          // First, find the 'Category' column object
          const categoryColumn = originalTable.columns.find(
            (col) => col.name === 'Category'
          );
          if (!categoryColumn) {
            throw new Error('Column "Category" not found for filtering.');
          }

          // Create a filter condition
          const filterCondition = categoryColumn.filter().eq('A');

          // Apply the filter. This creates a new view on the server.
          await originalTable.applyFilter([filterCondition]);
          console.log('Filter applied.');

          // Re-display the table data with a new title
          await displayTableData(
            originalTable,
            'Filtered Table Data (Category A)'
          );
        } catch (error) {
          throw new Error('Error applying filter', { cause: error });
        }
      }

      // Step 5: Main function that ties everything together
      async function main() {
        try {
          const client = await connectAndAuthenticate();

          const table = await openTable(client);

          // Display the unfiltered table
          await displayTableData(table, 'Unfiltered Table Data');

          // Apply a filter to the table (which also displays the filtered table)
          await applyFilterToTable(table);
        } catch (error) {
          console.error('Error occurred:', error);
        }
      }

      // Start the application
      main();
    </script>
  </body>
</html>

Conclusion

This crash course has walked you through the essentials of using the Deephaven JavaScript API to connect to a server, retrieve a table, display its data, and apply a simple filter. With these fundamentals, you can start exploring more advanced features and building more complex client applications.