npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@malloy-publisher/sdk

v0.4.1

Published

Malloy Publisher SDK

Readme

Malloy Publisher SDK

The Publisher SDK (@malloy-publisher/sdk) is a comprehensive React component library for building data applications that interact with Publisher's REST API. It provides everything you need to browse semantic models, execute queries, visualize results, and build interactive data experiences.

Table of Contents

  1. Installation
  2. Quick Start
  3. Core Concepts
  4. ServerProvider
  5. Page Components
  6. Query & Results Components
  7. Dimensional Filters
  8. Hooks
  9. Utilities
  10. Document Storage
  11. Styling
  12. Building a Custom Data App
  13. API Reference

Installation

# Using bun
bun add @malloy-publisher/sdk

# Using npm
npm install @malloy-publisher/sdk

# Using yarn
yarn add @malloy-publisher/sdk

Peer dependencies

The SDK expects the host to provide React and the Malloy packages it renders with, including @malloydata/malloy. The Malloy parser is only loaded when the dashboard builder opens (it is imported lazily, about 440 KB gzipped), so an app that never opens the builder never downloads it — but the package still has to be installed, or that one dynamic import fails at open time.

Quick Start

Basic Setup

import { ServerProvider, Home } from "@malloy-publisher/sdk";
import "@malloy-publisher/sdk/styles.css";

function App() {
   return (
      <ServerProvider baseURL="http://localhost:4000/api/v0">
         <Home
            onClickEnvironment={(path) => console.log("Navigate to:", path)}
         />
      </ServerProvider>
   );
}

With React Router

import {
   BrowserRouter,
   Routes,
   Route,
   useNavigate,
   useParams,
} from "react-router-dom";
import {
   ServerProvider,
   Home,
   Environment,
   Package,
   Model,
   Notebook,
   encodeResourceUri,
   useRouterClickHandler,
} from "@malloy-publisher/sdk";
import "@malloy-publisher/sdk/styles.css";

function App() {
   return (
      <ServerProvider>
         <BrowserRouter>
            <Routes>
               <Route path="/" element={<HomePage />} />
               <Route path="/:environmentName" element={<EnvironmentPage />} />
               <Route
                  path="/:environmentName/:packageName"
                  element={<PackagePage />}
               />
               <Route
                  path="/:environmentName/:packageName/*"
                  element={<ModelPage />}
               />
            </Routes>
         </BrowserRouter>
      </ServerProvider>
   );
}

function HomePage() {
   const navigate = useRouterClickHandler();
   return <Home onClickEnvironment={navigate} />;
}

function EnvironmentPage() {
   const navigate = useRouterClickHandler();
   const { environmentName } = useParams();
   const resourceUri = encodeResourceUri({ environmentName });
   return <Environment onSelectPackage={navigate} resourceUri={resourceUri} />;
}

function PackagePage() {
   const navigate = useRouterClickHandler();
   const { environmentName, packageName } = useParams();
   const resourceUri = encodeResourceUri({ environmentName, packageName });
   return <Package onClickPackageFile={navigate} resourceUri={resourceUri} />;
}

function ModelPage() {
   const params = useParams();
   const modelPath = params["*"];
   const resourceUri = encodeResourceUri({
      environmentName: params.environmentName,
      packageName: params.packageName,
      modelPath,
   });

   if (modelPath?.endsWith(".malloy")) {
      return <Model resourceUri={resourceUri} />;
   }
   if (modelPath?.endsWith(".malloynb")) {
      return <Notebook resourceUri={resourceUri} />;
   }
   return <div>Unknown file type</div>;
}

Core Concepts

Resource URIs

The SDK uses a standardized URI format to identify resources:

publisher://environments/{environmentName}/packages/{packageName}/models/{modelPath}?versionId={version}

Examples:

  • Environment: publisher://environments/my-environment
  • Package: publisher://environments/my-environment/packages/analytics
  • Model: publisher://environments/my-environment/packages/analytics/models/orders.malloy

Use the encodeResourceUri() and parseResourceUri() utilities to work with these URIs.

Component Hierarchy

The SDK components follow a natural hierarchy:

ServerProvider (required wrapper)
├── Home (list all environments)
│   └── Environment (show packages in an environment)
│       └── Package (show models, notebooks, connections)
│           ├── Model (visual query builder + named queries)
│           └── Notebook (read-only notebook viewer)

Navigation Pattern

Components accept callback functions for navigation rather than handling routing directly. This allows you to integrate with any routing solution:

// With React Router
const navigate = useRouterClickHandler();
<Home onClickEnvironment={navigate} />

// Custom navigation
<Home onClickEnvironment={(path) => window.location.href = path} />

// SPA with history
<Home onClickEnvironment={(path) => history.push(path)} />

ServerProvider

The ServerProvider is the required context provider that wraps your application. It initializes API clients and passes auth headers (if required by the backend server).

Props

| Prop | Type | Default | Description | | ---------------- | ----------------------- | ------------- | -------------------------------------------------------------------- | | baseURL | string | Auto-detected | Base URL of the Publisher API (e.g., http://localhost:4000/api/v0) | | getAccessToken | () => Promise<string> | undefined | Async function returning auth token | | mutable | boolean | true | Enable/disable environment/package management UI |

Basic Usage

<ServerProvider>{/* Your app */}</ServerProvider>

With Authentication

async function getAccessToken() {
   const response = await fetch("/auth/token");
   const { token } = await response.json();
   return `Bearer ${token}`;
}

<ServerProvider getAccessToken={getAccessToken}>
   {/* Your app */}
</ServerProvider>;

Read-Only Mode

// Disable add/edit/delete UI for production deployments
<ServerProvider mutable={false}>{/* Your app */}</ServerProvider>

Custom Server URL

<ServerProvider baseURL="https://publisher.example.com/api/v0">
   {/* Your app */}
</ServerProvider>

Page Components

Home

Displays a landing page with feature cards and a list of all available environments.

import { Home } from "@malloy-publisher/sdk";

interface HomeProps {
   onClickEnvironment?: (path: string, event?: React.MouseEvent) => void;
}

// Usage
<Home
   onClickEnvironment={(path, event) => {
      // path is like "/my-environment/"
      navigate(path);
   }}
/>;

Features:

  • Hero section with Publisher branding
  • Feature cards (Ad Hoc Analysis, Notebook Dashboards, AI Agents)
  • Environment listing with descriptions
  • Add/Edit/Delete environment dialogs (when mutable=true)

Environment

Shows all packages within an environment.

import { Environment, encodeResourceUri } from "@malloy-publisher/sdk";

interface EnvironmentProps {
   onSelectPackage: (path: string, event?: React.MouseEvent) => void;
   resourceUri: string;
}

// Usage
const resourceUri = encodeResourceUri({ environmentName: "my-environment" });

<Environment
   onSelectPackage={(path) => navigate(path)}
   resourceUri={resourceUri}
/>;

Features:

  • Package listing with version info
  • Add/Edit/Delete package dialogs (when mutable=true)
  • Environment README display

Package

Displays package details including models, notebooks, databases, and connections.

import { Package, encodeResourceUri } from "@malloy-publisher/sdk";

interface PackageProps {
   onClickPackageFile?: (path: string, event?: React.MouseEvent) => void;
   resourceUri: string;
}

// Usage
const resourceUri = encodeResourceUri({
   environmentName: "my-environment",
   packageName: "analytics",
});

<Package
   onClickPackageFile={(path) => navigate(path)}
   resourceUri={resourceUri}
/>;

Features:

  • Models list (.malloy files)
  • Notebooks list (.malloynb files)
  • Embedded databases (.parquet, .csv, and .xlsx files)
  • Connection configuration
  • Package README

Model

The visual query builder and model explorer. This is the primary component for ad-hoc data analysis.

import { Model, encodeResourceUri } from "@malloy-publisher/sdk";

interface ModelProps {
   resourceUri: string;
   onChange?: (query: QueryExplorerResult) => void;
   runOnDemand?: boolean; // Default: false
   maxResultSize?: number; // Default: 0 (no limit)
}

interface QueryExplorerResult {
   query: string | undefined;
   malloyQuery: Malloy.Query | string | undefined;
   malloyResult: Malloy.Result | undefined;
}

// Usage
const resourceUri = encodeResourceUri({
   environmentName: "my-environment",
   packageName: "analytics",
   modelPath: "models/orders.malloy",
});

<Model
   resourceUri={resourceUri}
   runOnDemand={true}
   maxResultSize={512 * 1024}
   onChange={(result) => {
      console.log("Query:", result.query);
      console.log("Result:", result.malloyResult);
   }}
/>;

Features:

  • Source selector (dropdown for models with multiple sources)
  • Visual query builder (Malloy Explorer integration)
  • Named queries display
  • Full-screen dialog mode
  • Copy link to current view

ModelExplorer

A lower-level component for embedding the query builder without the full Model chrome.

import {
   ModelExplorer,
   useModelData,
   encodeResourceUri,
} from "@malloy-publisher/sdk";

interface ModelExplorerProps {
   data?: CompiledModel; // Pre-loaded model data
   onChange?: (query: QueryExplorerResult) => void;
   existingQuery?: QueryExplorerResult; // Initialize with existing query
   initialSelectedSourceIndex?: number; // Default: 0
   onSourceChange?: (index: number) => void;
   resourceUri: string;
}

// Usage with automatic data loading
<ModelExplorer
   resourceUri={resourceUri}
   onChange={(result) => console.log(result)}
/>;

// Usage with pre-loaded data
const { data } = useModelData(resourceUri);

<ModelExplorer
   data={data}
   resourceUri={resourceUri}
   onChange={(result) => console.log(result)}
/>;

Notebook

Read-only notebook viewer that executes cells and displays results.

import { Notebook, encodeResourceUri } from "@malloy-publisher/sdk";

interface NotebookProps {
   resourceUri: string;
   maxResultSize?: number; // Default: 0 (no limit)
   // The notebook's `given:` parameters. Pass the ones you hold (from the URL,
   // say) and the notebook runs every cell with them.
   givens?: Record<string, string>;
   // Called when a parameter changes, with the new values and the names the
   // notebook manages, so a host can put them in the URL. `managed` is what to
   // clear when a value goes away; anything else in your URL is left alone.
   onGivensChange?: (
      givens: Record<string, string>,
      managed: readonly string[],
   ) => void;
   // Where a `# drill` click wants to go. Given no handler, a drill onto the
   // notebook's own parameters still works; only navigation away is dropped.
   onNavigate?: (to: string, event?: NavigationClick) => void;
}

// Usage
const resourceUri = encodeResourceUri({
   environmentName: "my-environment",
   packageName: "analytics",
   modelPath: "notebooks/sales-dashboard.malloynb",
});

<Notebook resourceUri={resourceUri} maxResultSize={1024 * 1024} />;

Features:

  • Sequential cell execution
  • Markdown rendering
  • Code cell execution with results
  • Error handling per cell

Query & Results Components

QueryResult

Executes a query and displays the visualization.

import { QueryResult, encodeResourceUri } from "@malloy-publisher/sdk";

interface QueryResultProps {
  query?: string;        // Raw Malloy query
  sourceName?: string;   // Source name for named query
  queryName?: string;    // Named query to execute
  resourceUri?: string;  // Resource URI for model
}

// Execute a named query
<QueryResult
  sourceName="orders"
  queryName="by_region"
  resourceUri={encodeResourceUri({
    environmentName: "my-environment",
    packageName: "analytics",
    modelPath: "models/orders.malloy",
  })}
/>

// Execute a raw query
<QueryResult
  query="run: orders -> { group_by: status; aggregate: order_count }"
  resourceUri={encodeResourceUri({
    environmentName: "my-environment",
    packageName: "analytics",
    modelPath: "models/orders.malloy",
  })}
/>

RenderedResult

Low-level component for rendering Malloy result JSON as a visualization.

import RenderedResult from "@malloy-publisher/sdk";

interface RenderedResultProps {
   result: string; // JSON result string
   height?: number; // Fixed height in pixels
   onSizeChange?: (height: number) => void; // Callback when size changes
   drill?: DrillBinding; // `# drill` click handling and its affordance
}

// Usage (result is the JSON string from query execution)
<RenderedResult
   result={queryResultJson}
   drill={{
      onClick: (payload) => {
         console.log("Clicked:", payload.field?.name, payload.value);
      },
      // Which fields' cells should read as clickable. Build the whole binding
      // with `useDrill` to get destination handling and the menu for free.
      canDrill: () => false,
   }}
/>;

EmbeddedQueryResult

Helper for embedding query results as serialized JSON (useful for storage/transfer).

import {
   EmbeddedQueryResult,
   createEmbeddedQueryResult,
} from "@malloy-publisher/sdk";

// Create embedded query config
const embedded = createEmbeddedQueryResult({
   queryName: "by_region",
   sourceName: "orders",
   resourceUri: encodeResourceUri({
      environmentName: "my-environment",
      packageName: "analytics",
      modelPath: "models/orders.malloy",
   }),
});

// Later, render it
<EmbeddedQueryResult embeddedQueryResult={embedded} />;

Dimensional Filters

The SDK supports interactive dimensional filtering for hand-built data apps. Filters are configured through annotations in Malloy source files. The Notebook component does not use this mechanism; see Notebooks use given: below.

Filter Types

| Type | UI Component | Use Case | | ------------ | --------------------- | ---------------------------------- | | Star | Multi-select dropdown | String fields with discrete values | | MinMax | Range slider | Numeric fields | | DateMinMax | Date range picker | Date/timestamp fields | | Retrieval | Semantic search input | Free-text semantic search | | Boolean | Toggle switch | Boolean fields |

Source Declaration Syntax

Add filter annotations to dimensions in your Malloy source files using the #(filter) tag:

source: flights is duckdb.table('data/flights.parquet') extend {
  dimension:
    // Multi-select dropdown for string values
    #(filter) {"type": "Star"}
    origin_code is origin

    // Range slider for numeric values
    #(filter) {"type": "MinMax"}
    distance_miles is distance

    // Date range picker
    #(filter) {"type": "DateMinMax"}
    flight_departure is dep_time

  join_one: carriers with carrier
}

source: carriers is duckdb.table('data/carriers.parquet') extend {
  dimension:
    #(filter) {"type": "Star"}
    nickname is nickname_old

    // Semantic search for text fields (requires embedding index)
    #(index_values) n=-1
    #(filter) {"type": "Retrieval"}
    name is name_old
}

source: recalls is duckdb.table('data/recalls.csv') extend {
  dimension:
    // Boolean toggle filter
    #(filter) {"type": "Boolean"}
    is_major_recall is potentially_affected > 100000
}

Custom Labels

By default, filters display the dimension field name in the UI. You can customize the display label using the # label="..." annotation:

source: recalls is duckdb.table('data/recalls.csv') extend {
  dimension:
    #(filter) {"type": "Star"}
    # label="Vehicle Manufacturer"
    Manufacturer is Manufacturer_old

    #(filter) {"type": "Retrieval"}
    # label="Recall Subject"
    Subject is Subject_old

    #(filter) {"type": "MinMax"}
    # label="Number of Affected Vehicles"
    potentially_affected is affected_count
}

The # label="..." annotation can be placed before or after the #(filter) annotation. When present, the label value will be displayed in the filter UI instead of the raw field name.

Notebooks use given:, not these annotations

The Notebook component no longer renders a filter panel from a ##(filters) annotation, and its retrievalFn prop is gone with it. A notebook's controls now come from the given: parameters its model declares, which is the mechanism described in docs/givens.md. A ##(filters) annotation in a notebook cell is inert.

The #(filter) source annotations above still work, and so do the hooks below. They are what a hand-built data app uses; only the notebook's own panel changed.

React Hooks for Programmatic Filtering

For custom data apps, use the SDK's React hooks:

import {
   useDimensionFiltersFromSpec,
   DimensionFiltersConfig,
} from "@malloy-publisher/sdk";

const config: DimensionFiltersConfig = {
   environment: "malloy-samples",
   package: "faa",
   indexLimit: 1000,
   dimensionSpecs: [
      {
         dimensionName: "origin_code",
         filterType: "Star",
         source: "flights",
         model: "flights.malloy",
         label: "Origin Airport",
      },
      {
         dimensionName: "distance",
         filterType: "MinMax",
         source: "flights",
         model: "flights.malloy",
         label: "Distance (miles)",
      },
      {
         dimensionName: "dep_time",
         filterType: "DateMinMax",
         source: "flights",
         model: "flights.malloy",
         label: "Departure Time",
      },
   ],
};

function FilteredDashboard() {
   const {
      filterStates, // Current filter values
      updateFilter, // Update a single filter
      clearAllFilters, // Reset all filters
      activeFilters, // Array of active filter selections
      data, // Dimension values for dropdowns/sliders
      isLoading, // Loading state
      executeQuery, // Run query with current filters
      queryString, // Generated Malloy query
   } = useDimensionFiltersFromSpec(config);

   // Render filter UI and results...
}

Match Types

Filters support different match types depending on the filter type:

| Match Type | Description | Applicable To | | ---------------------------- | ------------------------------------ | ------------------ | | Equals | Exact match (multi-select supported) | Star, Retrieval | | Contains | Substring match | Star | | Greater Than / Less Than | Comparison | MinMax | | Between | Range (inclusive) | MinMax, DateMinMax | | After / Before | Date comparison | DateMinMax | | Semantic Search | Semantic similarity | Retrieval |


Hooks

useServer

Access the server context (API clients, configuration).

import { useServer } from "@malloy-publisher/sdk";

function MyComponent() {
   const {
      server, // Base URL string
      apiClients, // API client instances
      mutable, // Whether mutations are allowed
      getAccessToken, // Auth token function
   } = useServer();

   // Use API clients directly
   const environments = await apiClients.environments.listEnvironments();
   const model = await apiClients.models.getModel(
      environmentName,
      packageName,
      modelPath,
      versionId,
   );
}

API Clients Available

interface ApiClients {
   models: ModelsApi; // Get/execute models
   environments: EnvironmentsApi; // CRUD environments
   packages: PackagesApi; // CRUD packages
   notebooks: NotebooksApi; // Get/execute notebooks
   connections: ConnectionsApi; // CRUD connections
   databases: DatabasesApi; // Access embedded databases
   watchMode: WatchModeApi; // File watching for dev
}

useQueryWithApiError

React Query wrapper with standardized error handling.

import { useQueryWithApiError } from "@malloy-publisher/sdk";

function MyComponent() {
   const { data, isLoading, isError, error } = useQueryWithApiError({
      queryKey: ["my-data", someParam],
      queryFn: async () => {
         const response = await apiClients.environments.listEnvironments();
         return response.data;
      },
   });

   if (isLoading) return <Loading />;
   if (isError) return <ApiErrorDisplay error={error} context="Loading data" />;
   return <div>{JSON.stringify(data)}</div>;
}

Features:

  • Automatic server-based cache key namespacing
  • Standardized axios error transformation
  • No automatic retries (explicit control)

useMutationWithApiError

Mutation wrapper with standardized error handling.

import { useMutationWithApiError } from "@malloy-publisher/sdk";

function MyComponent() {
   const mutation = useMutationWithApiError({
      mutationFn: async (newEnvironment) => {
         const response =
            await apiClients.environments.createEnvironment(newEnvironment);
         return response.data;
      },
      onSuccess: () => {
         queryClient.invalidateQueries(["environments"]);
      },
   });

   return (
      <button onClick={() => mutation.mutate({ name: "new-environment" })}>
         Create Environment
      </button>
   );
}

useModelData

Fetch compiled model data for a resource URI.

import { useModelData } from "@malloy-publisher/sdk";

function MyComponent({ resourceUri }) {
   const {
      data, // CompiledModel
      isLoading,
      isError,
      error,
   } = useModelData(resourceUri);

   if (isLoading) return <Loading text="Loading model..." />;
   if (isError) return <ApiErrorDisplay error={error} />;

   // Access model data
   console.log("Sources:", data.sourceInfos);
   console.log("Queries:", data.queries);
}

useRawQueryData

Execute a query and get raw data (array of rows) instead of visualization.

import { useRawQueryData } from "@malloy-publisher/sdk";

function MyComponent({ resourceUri }) {
   const {
      data, // Array of row objects
      isLoading,
      isSuccess,
      isError,
      error,
   } = useRawQueryData({
      resourceUri,
      modelPath: "models/orders.malloy",
      queryName: "by_region",
      sourceName: "orders",
      enabled: true,
   });

   if (isSuccess) {
      // data is an array of row objects
      data.forEach((row) => {
         console.log(row.region, row.total_sales);
      });
   }
}

useRouterClickHandler

Smart navigation hook that supports modifier keys (Cmd/Ctrl+click for new tab).

import { useRouterClickHandler } from "@malloy-publisher/sdk";

function MyComponent() {
   const navigate = useRouterClickHandler();

   return (
      <button onClick={(e) => navigate("/environments/analytics", e)}>
         Go to Analytics
      </button>
   );
}

Behavior:

  • Normal click: In-app navigation
  • Cmd/Ctrl+click: Open in new tab
  • Middle-click: Open in new tab
  • Shift+click: Open in new window

Utilities

encodeResourceUri

Create a resource URI from components.

import { encodeResourceUri } from "@malloy-publisher/sdk";

// Environment only
const environmentUri = encodeResourceUri({
   environmentName: "my-environment",
});
// Result: "publisher://environments/my-environment"

// Package
const packageUri = encodeResourceUri({
   environmentName: "my-environment",
   packageName: "analytics",
});
// Result: "publisher://environments/my-environment/packages/analytics"

// Model with version
const modelUri = encodeResourceUri({
   environmentName: "my-environment",
   packageName: "analytics",
   modelPath: "models/orders.malloy",
   versionId: "abc123",
});
// Result: "publisher://environments/my-environment/packages/analytics/models/models/orders.malloy?versionId=abc123"

parseResourceUri

Parse a resource URI back to components.

import { parseResourceUri } from "@malloy-publisher/sdk";

const uri =
   "publisher://environments/my-environment/packages/analytics/models/orders.malloy?versionId=abc123";
const parsed = parseResourceUri(uri);

// Result:
// {
//   environmentName: "my-environment",
//   packageName: "analytics",
//   modelPath: "orders.malloy",
//   versionId: "abc123"
// }

ParsedResource Type

type ParsedResource = {
   environmentName: string;
   packageName?: string;
   connectionName?: string;
   versionId?: string;
   modelPath?: string;
};

Document Storage

The SDK renders dashboards and notebooks a Publisher server serves out of a package, and it will author them too, but it does not decide where an authored document is kept. That is the host's choice, made by passing a DocumentStorage implementation into DocumentStorageProvider. The Console keeps documents in this browser's localStorage; a platform keeps them in its own document store; a repo-backed host writes them to the package directory.

DocumentStorage Interface

/** The kinds of document the SDK authors. */
type DocumentType = "dashboard" | "notebook";

interface Workspace {
   name: string;
   writeable: boolean;
   /** What this place is, in the backend's own words. The editor shows it. */
   description: string;
   /**
    * This workspace holds the package's system of record: the editor opens
    * what it keeps and writes back to it, and treats the package file as a
    * deploy of it. Left out, the package file is the record and this is a
    * place a copy is kept beside it, which is what every host got before the
    * flag existed. At most one workspace per storage sets it.
    */
   authoritative?: boolean;
}

interface DocumentLocator {
   workspace: string;
   type: DocumentType;
   path: string; // as the backend spells it, e.g. "dashboards/overview.malloy"
}

interface DocumentStorage {
   listWorkspaces(writeableOnly: boolean): Promise<Workspace[]>;
   listDocuments(
      workspace: Workspace,
      type?: DocumentType,
   ): Promise<DocumentLocator[]>;
   getDocument(locator: DocumentLocator): Promise<string>;
   saveDocument(locator: DocumentLocator, content: string): Promise<void>;
   deleteDocument(locator: DocumentLocator): Promise<void>;
   moveDocument(from: DocumentLocator, to: DocumentLocator): Promise<void>;
}

getDocument, deleteDocument and moveDocument reject with DocumentNotFoundError when the document is not there, and with anything else when the backend could not be asked. The distinction is load-bearing rather than cosmetic: a failed read reported as "there is no document" reads as "the package is the only copy", and saving on that belief overwrites the copy that was actually there. Use isDocumentNotFound(error) rather than instanceof, since the es and cjs builds carry their own copy of the class.

A document is a string; the type on its locator says what kind so a backend can keep kinds apart and a listing can ask for one. Every method rejects when the document is not there, so a caller can tell "missing" from "empty".

BrowserDocumentStorage

The default: one workspace named Local, private to this browser and origin.

import {
   BrowserDocumentStorage,
   DocumentStorageProvider,
} from "@malloy-publisher/sdk";

const storage = new BrowserDocumentStorage();

<DocumentStorageProvider documentStorage={storage}>
   <App />
</DocumentStorageProvider>;

A custom backend

Implement the interface over whatever you have. For example, over a document API keyed by workspace, type and path:

class ApiDocumentStorage implements DocumentStorage {
   constructor(private client: DocumentsApi) {}

   async listWorkspaces(writeableOnly: boolean): Promise<Workspace[]> {
      const all = await this.client.listWorkspaces();
      return writeableOnly ? all.filter((w) => w.writeable) : all;
   }

   async listDocuments(
      workspace: Workspace,
      type?: DocumentType,
   ): Promise<DocumentLocator[]> {
      const docs = await this.client.listDocuments(workspace.name, type);
      return docs.map((d) => ({
         workspace: workspace.name,
         type: d.type,
         path: d.path,
      }));
   }

   async getDocument(locator: DocumentLocator): Promise<string> {
      return (await this.client.getDocument(locator.workspace, locator.path))
         .content;
   }

   async saveDocument(
      locator: DocumentLocator,
      content: string,
   ): Promise<void> {
      await this.client.putDocument(locator.workspace, locator.path, {
         type: locator.type,
         content,
      });
   }

   async deleteDocument(locator: DocumentLocator): Promise<void> {
      await this.client.deleteDocument(locator.workspace, locator.path);
   }

   async moveDocument(
      from: DocumentLocator,
      to: DocumentLocator,
   ): Promise<void> {
      const content = await this.getDocument(from);
      await this.saveDocument(to, content);
      await this.deleteDocument(from);
   }
}

DocumentStorageProvider

import {
   DocumentStorageProvider,
   useDocumentStorage,
} from "@malloy-publisher/sdk";

<DocumentStorageProvider documentStorage={myStorage}>
   <App />
</DocumentStorageProvider>;

// Inside a component:
function MyComponent() {
   const { documentStorage } = useDocumentStorage();
   const dashboards = await documentStorage.listDocuments(
      { name: "Local", writeable: true, description: "" },
      "dashboard",
   );
}

Styling

Required CSS

Import the SDK styles in your app entry point:

// Main SDK styles (required)
import "@malloy-publisher/sdk/styles.css";

// If using Model/ModelExplorer outside of Publisher
import "@malloy-publisher/sdk/malloy-explorer.css";

Material-UI Theme

The SDK uses Material-UI (MUI) v7. You can customize the theme:

import { createTheme, ThemeProvider, CssBaseline } from "@mui/material";
import { ServerProvider } from "@malloy-publisher/sdk";

const theme = createTheme({
   palette: {
      primary: {
         main: "#14b3cb", // Malloy teal
      },
      secondary: {
         main: "#fbbb04", // Malloy yellow
      },
   },
   typography: {
      fontFamily: '"Inter", "Roboto", sans-serif',
   },
});

function App() {
   return (
      <ServerProvider>
         <ThemeProvider theme={theme}>
            <CssBaseline />
            {/* Your app */}
         </ThemeProvider>
      </ServerProvider>
   );
}

Styled Components

The SDK exports the pieces the Publisher Console is built from, so a host can build screens that match it:

import {
   AddButton, // the filled pill that adds one thing to a section
   SecondaryButton, // the outlined control beside it
   AppDialog, // every dialog, in one shape
   BackLink, // the way up, at the top of a page
   DashboardBar, // the bar above a dashboard, in both modes
   PALETTE, // the twelve hues everything meaningful is drawn from
   SURFACE_TINT, // which hue an environment, package or connection gets
} from "@malloy-publisher/sdk";

ItemRow and PackageSection, the row and section those screens are built out of, are deliberately internal for now; so are the styled helpers in components/styles.ts, which earlier versions of this README showed being imported, which never worked.


Building a Custom Data App

Example: Dashboard with Multiple Visualizations

import {
   ServerProvider,
   QueryResult,
   useModelData,
   encodeResourceUri,
   ApiErrorDisplay,
   Loading,
} from "@malloy-publisher/sdk";
import "@malloy-publisher/sdk/styles.css";
import { Grid, Typography, Paper } from "@mui/material";

function Dashboard() {
   const resourceUri = encodeResourceUri({
      environmentName: "my-environment",
      packageName: "analytics",
      modelPath: "models/sales.malloy",
   });

   const { data, isLoading, isError, error } = useModelData(resourceUri);

   if (isLoading) return <Loading text="Loading dashboard..." />;
   if (isError) return <ApiErrorDisplay error={error} context="Dashboard" />;

   return (
      <Grid container spacing={3}>
         <Grid item xs={12}>
            <Typography variant="h4">Sales Dashboard</Typography>
         </Grid>

         <Grid item xs={12} md={6}>
            <Paper sx={{ p: 2 }}>
               <Typography variant="h6">Sales by Region</Typography>
               <QueryResult
                  sourceName="orders"
                  queryName="by_region"
                  resourceUri={resourceUri}
               />
            </Paper>
         </Grid>

         <Grid item xs={12} md={6}>
            <Paper sx={{ p: 2 }}>
               <Typography variant="h6">Monthly Trends</Typography>
               <QueryResult
                  sourceName="orders"
                  queryName="monthly_trends"
                  resourceUri={resourceUri}
               />
            </Paper>
         </Grid>

         <Grid item xs={12}>
            <Paper sx={{ p: 2 }}>
               <Typography variant="h6">Custom Query</Typography>
               <QueryResult
                  query="run: orders -> {
              group_by: product_category
              aggregate:
                total_revenue is sum(revenue)
                avg_order_value is avg(order_value)
            }"
                  resourceUri={resourceUri}
               />
            </Paper>
         </Grid>
      </Grid>
   );
}

function App() {
   return (
      <ServerProvider baseURL="http://localhost:4000/api/v0">
         <Dashboard />
      </ServerProvider>
   );
}

Example: Data Table with Raw Query Data

import {
   ServerProvider,
   useRawQueryData,
   encodeResourceUri,
   Loading,
   ApiErrorDisplay,
} from "@malloy-publisher/sdk";
import { DataGrid } from "@mui/x-data-grid";

function DataTable() {
   const resourceUri = encodeResourceUri({
      environmentName: "my-environment",
      packageName: "analytics",
      modelPath: "models/customers.malloy",
   });

   const { data, isLoading, isError, error } = useRawQueryData({
      resourceUri,
      modelPath: "models/customers.malloy",
      sourceName: "customers",
      queryName: "all_customers",
   });

   if (isLoading) return <Loading text="Loading data..." />;
   if (isError) return <ApiErrorDisplay error={error} />;

   const columns =
      data.length > 0
         ? Object.keys(data[0]).map((key) => ({
              field: key,
              headerName: key,
              width: 150,
           }))
         : [];

   return (
      <DataGrid
         rows={data.map((row, i) => ({ id: i, ...row }))}
         columns={columns}
         pageSize={10}
         autoHeight
      />
   );
}

Example: Interactive Model Explorer

import {
   ServerProvider,
   ModelExplorer,
   encodeResourceUri,
} from "@malloy-publisher/sdk";
import "@malloy-publisher/sdk/styles.css";
import "@malloy-publisher/sdk/malloy-explorer.css";
import { useState } from "react";

function Explorer() {
   const [selectedQuery, setSelectedQuery] = useState(null);

   const resourceUri = encodeResourceUri({
      environmentName: "my-environment",
      packageName: "analytics",
      modelPath: "models/orders.malloy",
   });

   return (
      <div style={{ display: "flex", gap: "20px" }}>
         <div style={{ flex: 1 }}>
            <h2>Build Your Query</h2>
            <ModelExplorer
               resourceUri={resourceUri}
               onChange={(result) => {
                  setSelectedQuery(result);
                  console.log("Generated Query:", result.query);
               }}
            />
         </div>

         {selectedQuery && (
            <div style={{ flex: 1 }}>
               <h2>Query Preview</h2>
               <pre>{selectedQuery.query}</pre>
            </div>
         )}
      </div>
   );
}

Example: Lightweight Client-Only Setup

For minimal bundle size when you only need API access:

// Use the client entry point
import { ServerProvider, useServer } from "@malloy-publisher/sdk/client";

function MyApp() {
   return (
      <ServerProvider baseURL="http://localhost:4000/api/v0">
         <EnvironmentList />
      </ServerProvider>
   );
}

function EnvironmentList() {
   const { apiClients } = useServer();
   const [environments, setEnvironments] = useState([]);

   useEffect(() => {
      apiClients.environments
         .listEnvironments()
         .then((response) => setEnvironments(response.data));
   }, []);

   return (
      <ul>
         {environments.map((p) => (
            <li key={p.name}>{p.name}</li>
         ))}
      </ul>
   );
}

API Reference

Exported Components

| Component | Description | | ------------------------- | ----------------------------------------------- | | ServerProvider | Required context provider for API access | | Home | Environment listing landing page | | Environment | Package listing for an environment | | Package | Package detail (models, notebooks, connections) | | Model | Full model explorer with visual query builder | | ModelExplorer | Lower-level query builder component | | ModelExplorerDialog | Model explorer in a modal dialog | | Notebook | Read-only notebook viewer | | DocumentStorageProvider | Context for document storage | | QueryResult | Execute and display query | | RenderedResult | Render Malloy result JSON | | EmbeddedQueryResult | Render serialized query config | | Loading | Loading spinner with text | | ApiErrorDisplay | Error display component | | SourcesExplorer | Source schema browser | | ConnectionExplorer | Connection management UI |

Exported Hooks

| Hook | Description | | ----------------------------- | ----------------------------------- | | useServer | Access ServerProvider context | | useQueryWithApiError | React Query with error handling | | useMutationWithApiError | Mutations with error handling | | useModelData | Fetch compiled model | | useRawQueryData | Execute query, get raw data | | useRouterClickHandler | Smart navigation with modifier keys | | useDocumentStorage | Access document storage context | | useDimensionFiltersFromSpec | Programmatic dimensional filtering |

Exported Utilities

| Utility | Description | | --------------------------- | ----------------------------------- | | encodeResourceUri | Create resource URI from components | | parseResourceUri | Parse resource URI to components | | createEmbeddedQueryResult | Serialize query config | | BrowserDocumentStorage | localStorage-based document storage | | globalQueryClient | Shared React Query client | | DocumentNotFoundError | Absence, not a failed read | | isDocumentNotFound | Absence check across es/cjs builds |

Exported Types

| Type | Description | | ------------------------ | -------------------------------- | | ParsedResource | Parsed resource URI components | | ServerContextValue | Server context interface | | ServerProviderProps | ServerProvider props | | QueryExplorerResult | Query builder result | | SourceAndPath | Source info with model path | | DocumentStorage | Document storage interface | | DocumentLocator | Workspace + type + path | | DocumentType | "dashboard" or "notebook" | | Workspace | Workspace metadata | | ApiError | Standardized API error | | ModelExplorerProps | ModelExplorer props | | DimensionFiltersConfig | Dimensional filter configuration |


Additional Resources