@malloy-publisher/sdk 0.0.140 → 0.0.142

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,24 +1,584 @@
1
1
  # Malloy Publisher SDK
2
2
 
3
- The Malloy Publisher SDK is a comprehensive toolkit designed to facilitate the development and testing of Malloy packages.
3
+ 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.
4
4
 
5
- ## CSS
6
- To pull in CSS styling required by various SDK components do:
7
- ```ts
8
- // Import required CSS
9
- import '@malloy-publisher/sdk/styles.css';
5
+ ## Table of Contents
6
+
7
+ 1. [Installation](#installation)
8
+ 2. [Quick Start](#quick-start)
9
+ 3. [Core Concepts](#core-concepts)
10
+ 4. [ServerProvider](#serverprovider)
11
+ 5. [Page Components](#page-components)
12
+ 6. [Query & Results Components](#query--results-components)
13
+ 7. [Dimensional Filters](#dimensional-filters)
14
+ 8. [Hooks](#hooks)
15
+ 9. [Utilities](#utilities)
16
+ 10. [Workbook Storage](#workbook-storage)
17
+ 11. [Styling](#styling)
18
+ 12. [Building a Custom Data App](#building-a-custom-data-app)
19
+ 13. [API Reference](#api-reference)
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ # Using bun
27
+ bun add @malloy-publisher/sdk
28
+
29
+ # Using npm
30
+ npm install @malloy-publisher/sdk
31
+
32
+ # Using yarn
33
+ yarn add @malloy-publisher/sdk
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Quick Start
39
+
40
+ ### Basic Setup
41
+
42
+ ```tsx
43
+ import { ServerProvider, Home } from "@malloy-publisher/sdk";
44
+ import "@malloy-publisher/sdk/styles.css";
45
+
46
+ function App() {
47
+ return (
48
+ <ServerProvider baseURL="http://localhost:4000/api/v0">
49
+ <Home onClickProject={(path) => console.log("Navigate to:", path)} />
50
+ </ServerProvider>
51
+ );
52
+ }
53
+ ```
54
+
55
+ ### With React Router
56
+
57
+ ```tsx
58
+ import {
59
+ BrowserRouter,
60
+ Routes,
61
+ Route,
62
+ useNavigate,
63
+ useParams,
64
+ } from "react-router-dom";
65
+ import {
66
+ ServerProvider,
67
+ Home,
68
+ Project,
69
+ Package,
70
+ Model,
71
+ Notebook,
72
+ encodeResourceUri,
73
+ useRouterClickHandler,
74
+ } from "@malloy-publisher/sdk";
75
+ import "@malloy-publisher/sdk/styles.css";
76
+
77
+ function App() {
78
+ return (
79
+ <ServerProvider>
80
+ <BrowserRouter>
81
+ <Routes>
82
+ <Route path="/" element={<HomePage />} />
83
+ <Route path="/:projectName" element={<ProjectPage />} />
84
+ <Route
85
+ path="/:projectName/:packageName"
86
+ element={<PackagePage />}
87
+ />
88
+ <Route
89
+ path="/:projectName/:packageName/*"
90
+ element={<ModelPage />}
91
+ />
92
+ </Routes>
93
+ </BrowserRouter>
94
+ </ServerProvider>
95
+ );
96
+ }
97
+
98
+ function HomePage() {
99
+ const navigate = useRouterClickHandler();
100
+ return <Home onClickProject={navigate} />;
101
+ }
102
+
103
+ function ProjectPage() {
104
+ const navigate = useRouterClickHandler();
105
+ const { projectName } = useParams();
106
+ const resourceUri = encodeResourceUri({ projectName });
107
+ return <Project onSelectPackage={navigate} resourceUri={resourceUri} />;
108
+ }
109
+
110
+ function PackagePage() {
111
+ const navigate = useRouterClickHandler();
112
+ const { projectName, packageName } = useParams();
113
+ const resourceUri = encodeResourceUri({ projectName, packageName });
114
+ return <Package onClickPackageFile={navigate} resourceUri={resourceUri} />;
115
+ }
116
+
117
+ function ModelPage() {
118
+ const params = useParams();
119
+ const modelPath = params["*"];
120
+ const resourceUri = encodeResourceUri({
121
+ projectName: params.projectName,
122
+ packageName: params.packageName,
123
+ modelPath,
124
+ });
125
+
126
+ if (modelPath?.endsWith(".malloy")) {
127
+ return <Model resourceUri={resourceUri} />;
128
+ }
129
+ if (modelPath?.endsWith(".malloynb")) {
130
+ return <Notebook resourceUri={resourceUri} />;
131
+ }
132
+ return <div>Unknown file type</div>;
133
+ }
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Core Concepts
139
+
140
+ ### Resource URIs
141
+
142
+ The SDK uses a standardized URI format to identify resources:
143
+
144
+ ```
145
+ publisher://projects/{projectName}/packages/{packageName}/models/{modelPath}?versionId={version}
146
+ ```
147
+
148
+ Examples:
149
+
150
+ - Project: `publisher://projects/my-project`
151
+ - Package: `publisher://projects/my-project/packages/analytics`
152
+ - Model: `publisher://projects/my-project/packages/analytics/models/orders.malloy`
153
+
154
+ Use the `encodeResourceUri()` and `parseResourceUri()` utilities to work with these URIs.
155
+
156
+ ### Component Hierarchy
157
+
158
+ The SDK components follow a natural hierarchy:
159
+
160
+ ```
161
+ ServerProvider (required wrapper)
162
+ ├── Home (list all projects)
163
+ │ └── Project (show packages in a project)
164
+ │ └── Package (show models, notebooks, connections)
165
+ │ ├── Model (visual query builder + named queries)
166
+ │ └── Notebook (read-only notebook viewer)
167
+ └── Workbook (interactive analysis workbook)
168
+ ```
169
+
170
+ ### Navigation Pattern
171
+
172
+ Components accept callback functions for navigation rather than handling routing directly. This allows you to integrate with any routing solution:
173
+
174
+ ```tsx
175
+ // With React Router
176
+ const navigate = useRouterClickHandler();
177
+ <Home onClickProject={navigate} />
178
+
179
+ // Custom navigation
180
+ <Home onClickProject={(path) => window.location.href = path} />
181
+
182
+ // SPA with history
183
+ <Home onClickProject={(path) => history.push(path)} />
184
+ ```
185
+
186
+ ---
187
+
188
+ ## ServerProvider
189
+
190
+ 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).
191
+
192
+ ### Props
193
+
194
+ | Prop | Type | Default | Description |
195
+ | ---------------- | ----------------------- | ------------- | -------------------------------------------------------------------- |
196
+ | `baseURL` | `string` | Auto-detected | Base URL of the Publisher API (e.g., `http://localhost:4000/api/v0`) |
197
+ | `getAccessToken` | `() => Promise<string>` | `undefined` | Async function returning auth token |
198
+ | `mutable` | `boolean` | `true` | Enable/disable project/package management UI |
199
+
200
+ ### Basic Usage
201
+
202
+ ```tsx
203
+ <ServerProvider>{/* Your app */}</ServerProvider>
204
+ ```
205
+
206
+ ### With Authentication
207
+
208
+ ```tsx
209
+ async function getAccessToken() {
210
+ const response = await fetch("/auth/token");
211
+ const { token } = await response.json();
212
+ return `Bearer ${token}`;
213
+ }
214
+
215
+ <ServerProvider getAccessToken={getAccessToken}>
216
+ {/* Your app */}
217
+ </ServerProvider>;
218
+ ```
219
+
220
+ ### Read-Only Mode
221
+
222
+ ```tsx
223
+ // Disable add/edit/delete UI for production deployments
224
+ <ServerProvider mutable={false}>{/* Your app */}</ServerProvider>
225
+ ```
226
+
227
+ ### Custom Server URL
228
+
229
+ ```tsx
230
+ <ServerProvider baseURL="https://publisher.example.com/api/v0">
231
+ {/* Your app */}
232
+ </ServerProvider>
233
+ ```
234
+
235
+ ---
236
+
237
+ ## Page Components
238
+
239
+ ### Home
240
+
241
+ Displays a landing page with feature cards and a list of all available projects.
242
+
243
+ ```tsx
244
+ import { Home } from "@malloy-publisher/sdk";
245
+
246
+ interface HomeProps {
247
+ onClickProject?: (path: string, event?: React.MouseEvent) => void;
248
+ }
249
+
250
+ // Usage
251
+ <Home
252
+ onClickProject={(path, event) => {
253
+ // path is like "/my-project/"
254
+ navigate(path);
255
+ }}
256
+ />;
257
+ ```
258
+
259
+ **Features:**
260
+
261
+ - Hero section with Publisher branding
262
+ - Feature cards (Ad Hoc Analysis, Notebook Dashboards, AI Agents)
263
+ - Project listing with descriptions
264
+ - Add/Edit/Delete project dialogs (when `mutable=true`)
265
+
266
+ ---
267
+
268
+ ### Project
269
+
270
+ Shows all packages within a project.
271
+
272
+ ```tsx
273
+ import { Project, encodeResourceUri } from "@malloy-publisher/sdk";
274
+
275
+ interface ProjectProps {
276
+ onSelectPackage: (path: string, event?: React.MouseEvent) => void;
277
+ resourceUri: string;
278
+ }
279
+
280
+ // Usage
281
+ const resourceUri = encodeResourceUri({ projectName: "my-project" });
282
+
283
+ <Project
284
+ onSelectPackage={(path) => navigate(path)}
285
+ resourceUri={resourceUri}
286
+ />;
287
+ ```
288
+
289
+ **Features:**
290
+
291
+ - Package listing with version info
292
+ - Add/Edit/Delete package dialogs (when `mutable=true`)
293
+ - Project README display
294
+
295
+ ---
296
+
297
+ ### Package
298
+
299
+ Displays package details including models, notebooks, databases, and connections.
300
+
301
+ ```tsx
302
+ import { Package, encodeResourceUri } from "@malloy-publisher/sdk";
303
+
304
+ interface PackageProps {
305
+ onClickPackageFile?: (path: string, event?: React.MouseEvent) => void;
306
+ resourceUri: string;
307
+ }
308
+
309
+ // Usage
310
+ const resourceUri = encodeResourceUri({
311
+ projectName: "my-project",
312
+ packageName: "analytics",
313
+ });
314
+
315
+ <Package
316
+ onClickPackageFile={(path) => navigate(path)}
317
+ resourceUri={resourceUri}
318
+ />;
10
319
  ```
11
320
 
12
- ## Example Usage:
13
- **Rendering a project**
14
- ```react
15
- <Project name="malloy-samples" navigate={navigate} />
16
- ```
17
- **Rendering a package**
18
- ```react
19
- <Package name="ecommerce" projectName="malloy-samples" navigate={navigate} />
321
+ **Features:**
322
+
323
+ - Models list (`.malloy` files)
324
+ - Notebooks list (`.malloynb` files)
325
+ - Embedded databases (`.parquet` files)
326
+ - Connection configuration
327
+ - Package README
328
+
329
+ ---
330
+
331
+ ### Model
332
+
333
+ The visual query builder and model explorer. This is the primary component for ad-hoc data analysis.
334
+
335
+ ```tsx
336
+ import { Model, encodeResourceUri } from "@malloy-publisher/sdk";
337
+
338
+ interface ModelProps {
339
+ resourceUri: string;
340
+ onChange?: (query: QueryExplorerResult) => void;
341
+ runOnDemand?: boolean; // Default: false
342
+ maxResultSize?: number; // Default: 0 (no limit)
343
+ }
344
+
345
+ interface QueryExplorerResult {
346
+ query: string | undefined;
347
+ malloyQuery: Malloy.Query | string | undefined;
348
+ malloyResult: Malloy.Result | undefined;
349
+ }
350
+
351
+ // Usage
352
+ const resourceUri = encodeResourceUri({
353
+ projectName: "my-project",
354
+ packageName: "analytics",
355
+ modelPath: "models/orders.malloy",
356
+ });
357
+
358
+ <Model
359
+ resourceUri={resourceUri}
360
+ runOnDemand={true}
361
+ maxResultSize={512 * 1024}
362
+ onChange={(result) => {
363
+ console.log("Query:", result.query);
364
+ console.log("Result:", result.malloyResult);
365
+ }}
366
+ />;
20
367
  ```
21
368
 
369
+ **Features:**
370
+
371
+ - Source selector (dropdown for models with multiple sources)
372
+ - Visual query builder (Malloy Explorer integration)
373
+ - Named queries display
374
+ - Full-screen dialog mode
375
+ - Copy link to current view
376
+
377
+ ---
378
+
379
+ ### ModelExplorer
380
+
381
+ A lower-level component for embedding the query builder without the full Model chrome.
382
+
383
+ ```tsx
384
+ import {
385
+ ModelExplorer,
386
+ useModelData,
387
+ encodeResourceUri,
388
+ } from "@malloy-publisher/sdk";
389
+
390
+ interface ModelExplorerProps {
391
+ data?: CompiledModel; // Pre-loaded model data
392
+ onChange?: (query: QueryExplorerResult) => void;
393
+ existingQuery?: QueryExplorerResult; // Initialize with existing query
394
+ initialSelectedSourceIndex?: number; // Default: 0
395
+ onSourceChange?: (index: number) => void;
396
+ resourceUri: string;
397
+ }
398
+
399
+ // Usage with automatic data loading
400
+ <ModelExplorer
401
+ resourceUri={resourceUri}
402
+ onChange={(result) => console.log(result)}
403
+ />;
404
+
405
+ // Usage with pre-loaded data
406
+ const { data } = useModelData(resourceUri);
407
+
408
+ <ModelExplorer
409
+ data={data}
410
+ resourceUri={resourceUri}
411
+ onChange={(result) => console.log(result)}
412
+ />;
413
+ ```
414
+
415
+ ---
416
+
417
+ ### Notebook
418
+
419
+ Read-only notebook viewer that executes cells and displays results.
420
+
421
+ ```tsx
422
+ import { Notebook, encodeResourceUri } from "@malloy-publisher/sdk";
423
+
424
+ interface NotebookProps {
425
+ resourceUri: string;
426
+ maxResultSize?: number; // Default: 0 (no limit)
427
+ }
428
+
429
+ // Usage
430
+ const resourceUri = encodeResourceUri({
431
+ projectName: "my-project",
432
+ packageName: "analytics",
433
+ modelPath: "notebooks/sales-dashboard.malloynb",
434
+ });
435
+
436
+ <Notebook resourceUri={resourceUri} maxResultSize={1024 * 1024} />;
437
+ ```
438
+
439
+ **Features:**
440
+
441
+ - Sequential cell execution
442
+ - Markdown rendering
443
+ - Code cell execution with results
444
+ - Error handling per cell
445
+
446
+ ---
447
+
448
+ ### Workbook
449
+
450
+ Interactive workbook editor for creating and saving custom analyses.
451
+
452
+ ```tsx
453
+ import {
454
+ Workbook,
455
+ WorkbookStorageProvider,
456
+ BrowserWorkbookStorage,
457
+ encodeResourceUri,
458
+ } from "@malloy-publisher/sdk";
459
+
460
+ interface WorkbookProps {
461
+ workbookPath?: WorkbookLocator; // { path: string, workspace: string }
462
+ resourceUri: string;
463
+ }
464
+
465
+ // Usage
466
+ const workbookStorage = new BrowserWorkbookStorage();
467
+ const resourceUri = encodeResourceUri({
468
+ projectName: "my-project",
469
+ packageName: "analytics",
470
+ });
471
+
472
+ <WorkbookStorageProvider workbookStorage={workbookStorage}>
473
+ <Workbook
474
+ workbookPath={{ path: "my-analysis", workspace: "Local" }}
475
+ resourceUri={resourceUri}
476
+ />
477
+ </WorkbookStorageProvider>;
478
+ ```
479
+
480
+ **Features:**
481
+
482
+ - Add/remove Markdown and Malloy cells
483
+ - Model picker for source selection
484
+ - Auto-save to storage backend
485
+ - Export to Malloy format
486
+ - Delete workbook
487
+
488
+ ---
489
+
490
+ ## Query & Results Components
491
+
492
+ ### QueryResult
493
+
494
+ Executes a query and displays the visualization.
495
+
496
+ ```tsx
497
+ import { QueryResult, encodeResourceUri } from "@malloy-publisher/sdk";
498
+
499
+ interface QueryResultProps {
500
+ query?: string; // Raw Malloy query
501
+ sourceName?: string; // Source name for named query
502
+ queryName?: string; // Named query to execute
503
+ resourceUri?: string; // Resource URI for model
504
+ }
505
+
506
+ // Execute a named query
507
+ <QueryResult
508
+ sourceName="orders"
509
+ queryName="by_region"
510
+ resourceUri={encodeResourceUri({
511
+ projectName: "my-project",
512
+ packageName: "analytics",
513
+ modelPath: "models/orders.malloy",
514
+ })}
515
+ />
516
+
517
+ // Execute a raw query
518
+ <QueryResult
519
+ query="run: orders -> { group_by: status; aggregate: order_count }"
520
+ resourceUri={encodeResourceUri({
521
+ projectName: "my-project",
522
+ packageName: "analytics",
523
+ modelPath: "models/orders.malloy",
524
+ })}
525
+ />
526
+ ```
527
+
528
+ ---
529
+
530
+ ### RenderedResult
531
+
532
+ Low-level component for rendering Malloy result JSON as a visualization.
533
+
534
+ ```tsx
535
+ import RenderedResult from "@malloy-publisher/sdk";
536
+
537
+ interface RenderedResultProps {
538
+ result: string; // JSON result string
539
+ height?: number; // Fixed height in pixels
540
+ onSizeChange?: (height: number) => void; // Callback when size changes
541
+ onDrill?: (element: unknown) => void; // Drill-down callback
542
+ }
543
+
544
+ // Usage (result is the JSON string from query execution)
545
+ <RenderedResult
546
+ result={queryResultJson}
547
+ onDrill={(element) => {
548
+ console.log("Drilled into:", element);
549
+ }}
550
+ />;
551
+ ```
552
+
553
+ ---
554
+
555
+ ### EmbeddedQueryResult
556
+
557
+ Helper for embedding query results as serialized JSON (useful for storage/transfer).
558
+
559
+ ```tsx
560
+ import {
561
+ EmbeddedQueryResult,
562
+ createEmbeddedQueryResult,
563
+ } from "@malloy-publisher/sdk";
564
+
565
+ // Create embedded query config
566
+ const embedded = createEmbeddedQueryResult({
567
+ queryName: "by_region",
568
+ sourceName: "orders",
569
+ resourceUri: encodeResourceUri({
570
+ projectName: "my-project",
571
+ packageName: "analytics",
572
+ modelPath: "models/orders.malloy",
573
+ }),
574
+ });
575
+
576
+ // Later, render it
577
+ <EmbeddedQueryResult embeddedQueryResult={embedded} />;
578
+ ```
579
+
580
+ ---
581
+
22
582
  ## Dimensional Filters
23
583
 
24
584
  The SDK supports interactive dimensional filtering for notebooks and embedded data apps. Filters are configured through annotations in Malloy source files and notebooks.
@@ -95,9 +655,9 @@ If no search function is supplied, the filter is ignored.
95
655
  For custom data apps, use the SDK's React hooks:
96
656
 
97
657
  ```tsx
98
- import {
658
+ import {
99
659
  useDimensionFiltersFromSpec,
100
- DimensionFiltersConfig
660
+ DimensionFiltersConfig
101
661
  } from '@malloy-publisher/sdk';
102
662
 
103
663
  const config: DimensionFiltersConfig = {
@@ -138,4 +698,777 @@ Filters support different match types depending on the filter type:
138
698
  | `Greater Than` / `Less Than` | Comparison | MinMax |
139
699
  | `Between` | Range (inclusive) | MinMax, DateMinMax |
140
700
  | `After` / `Before` | Date comparison | DateMinMax |
141
- | `Concept Search` | Semantic similarity | Retrieval |
701
+ | `Concept Search` | Semantic similarity | Retrieval |
702
+
703
+ ---
704
+
705
+ ## Hooks
706
+
707
+ ### useServer
708
+
709
+ Access the server context (API clients, configuration).
710
+
711
+ ```tsx
712
+ import { useServer } from "@malloy-publisher/sdk";
713
+
714
+ function MyComponent() {
715
+ const {
716
+ server, // Base URL string
717
+ apiClients, // API client instances
718
+ mutable, // Whether mutations are allowed
719
+ getAccessToken, // Auth token function
720
+ } = useServer();
721
+
722
+ // Use API clients directly
723
+ const projects = await apiClients.projects.listProjects();
724
+ const model = await apiClients.models.getModel(
725
+ projectName,
726
+ packageName,
727
+ modelPath,
728
+ versionId,
729
+ );
730
+ }
731
+ ```
732
+
733
+ ### API Clients Available
734
+
735
+ ```typescript
736
+ interface ApiClients {
737
+ models: ModelsApi; // Get/execute models
738
+ projects: ProjectsApi; // CRUD projects
739
+ packages: PackagesApi; // CRUD packages
740
+ notebooks: NotebooksApi; // Get/execute notebooks
741
+ connections: ConnectionsApi; // CRUD connections
742
+ databases: DatabasesApi; // Access embedded databases
743
+ watchMode: WatchModeApi; // File watching for dev
744
+ }
745
+ ```
746
+
747
+ ---
748
+
749
+ ### useQueryWithApiError
750
+
751
+ React Query wrapper with standardized error handling.
752
+
753
+ ```tsx
754
+ import { useQueryWithApiError } from "@malloy-publisher/sdk";
755
+
756
+ function MyComponent() {
757
+ const { data, isLoading, isError, error } = useQueryWithApiError({
758
+ queryKey: ["my-data", someParam],
759
+ queryFn: async () => {
760
+ const response = await apiClients.projects.listProjects();
761
+ return response.data;
762
+ },
763
+ });
764
+
765
+ if (isLoading) return <Loading />;
766
+ if (isError) return <ApiErrorDisplay error={error} context="Loading data" />;
767
+ return <div>{JSON.stringify(data)}</div>;
768
+ }
769
+ ```
770
+
771
+ **Features:**
772
+
773
+ - Automatic server-based cache key namespacing
774
+ - Standardized axios error transformation
775
+ - No automatic retries (explicit control)
776
+
777
+ ---
778
+
779
+ ### useMutationWithApiError
780
+
781
+ Mutation wrapper with standardized error handling.
782
+
783
+ ```tsx
784
+ import { useMutationWithApiError } from "@malloy-publisher/sdk";
785
+
786
+ function MyComponent() {
787
+ const mutation = useMutationWithApiError({
788
+ mutationFn: async (newProject) => {
789
+ const response = await apiClients.projects.createProject(newProject);
790
+ return response.data;
791
+ },
792
+ onSuccess: () => {
793
+ queryClient.invalidateQueries(["projects"]);
794
+ },
795
+ });
796
+
797
+ return (
798
+ <button onClick={() => mutation.mutate({ name: "new-project" })}>
799
+ Create Project
800
+ </button>
801
+ );
802
+ }
803
+ ```
804
+
805
+ ---
806
+
807
+ ### useModelData
808
+
809
+ Fetch compiled model data for a resource URI.
810
+
811
+ ```tsx
812
+ import { useModelData } from "@malloy-publisher/sdk";
813
+
814
+ function MyComponent({ resourceUri }) {
815
+ const {
816
+ data, // CompiledModel
817
+ isLoading,
818
+ isError,
819
+ error,
820
+ } = useModelData(resourceUri);
821
+
822
+ if (isLoading) return <Loading text="Loading model..." />;
823
+ if (isError) return <ApiErrorDisplay error={error} />;
824
+
825
+ // Access model data
826
+ console.log("Sources:", data.sourceInfos);
827
+ console.log("Queries:", data.queries);
828
+ }
829
+ ```
830
+
831
+ ---
832
+
833
+ ### useRawQueryData
834
+
835
+ Execute a query and get raw data (array of rows) instead of visualization.
836
+
837
+ ```tsx
838
+ import { useRawQueryData } from "@malloy-publisher/sdk";
839
+
840
+ function MyComponent({ resourceUri }) {
841
+ const {
842
+ data, // Array of row objects
843
+ isLoading,
844
+ isSuccess,
845
+ isError,
846
+ error,
847
+ } = useRawQueryData({
848
+ resourceUri,
849
+ modelPath: "models/orders.malloy",
850
+ queryName: "by_region",
851
+ sourceName: "orders",
852
+ enabled: true,
853
+ });
854
+
855
+ if (isSuccess) {
856
+ // data is an array of row objects
857
+ data.forEach((row) => {
858
+ console.log(row.region, row.total_sales);
859
+ });
860
+ }
861
+ }
862
+ ```
863
+
864
+ ---
865
+
866
+ ### useRouterClickHandler
867
+
868
+ Smart navigation hook that supports modifier keys (Cmd/Ctrl+click for new tab).
869
+
870
+ ```tsx
871
+ import { useRouterClickHandler } from "@malloy-publisher/sdk";
872
+
873
+ function MyComponent() {
874
+ const navigate = useRouterClickHandler();
875
+
876
+ return (
877
+ <button onClick={(e) => navigate("/projects/analytics", e)}>
878
+ Go to Analytics
879
+ </button>
880
+ );
881
+ }
882
+ ```
883
+
884
+ **Behavior:**
885
+
886
+ - Normal click: In-app navigation
887
+ - Cmd/Ctrl+click: Open in new tab
888
+ - Middle-click: Open in new tab
889
+ - Shift+click: Open in new window
890
+
891
+ ---
892
+
893
+ ## Utilities
894
+
895
+ ### encodeResourceUri
896
+
897
+ Create a resource URI from components.
898
+
899
+ ```tsx
900
+ import { encodeResourceUri } from "@malloy-publisher/sdk";
901
+
902
+ // Project only
903
+ const projectUri = encodeResourceUri({
904
+ projectName: "my-project",
905
+ });
906
+ // Result: "publisher://projects/my-project"
907
+
908
+ // Package
909
+ const packageUri = encodeResourceUri({
910
+ projectName: "my-project",
911
+ packageName: "analytics",
912
+ });
913
+ // Result: "publisher://projects/my-project/packages/analytics"
914
+
915
+ // Model with version
916
+ const modelUri = encodeResourceUri({
917
+ projectName: "my-project",
918
+ packageName: "analytics",
919
+ modelPath: "models/orders.malloy",
920
+ versionId: "abc123",
921
+ });
922
+ // Result: "publisher://projects/my-project/packages/analytics/models/models/orders.malloy?versionId=abc123"
923
+ ```
924
+
925
+ ---
926
+
927
+ ### parseResourceUri
928
+
929
+ Parse a resource URI back to components.
930
+
931
+ ```tsx
932
+ import { parseResourceUri } from "@malloy-publisher/sdk";
933
+
934
+ const uri =
935
+ "publisher://projects/my-project/packages/analytics/models/orders.malloy?versionId=abc123";
936
+ const parsed = parseResourceUri(uri);
937
+
938
+ // Result:
939
+ // {
940
+ // projectName: "my-project",
941
+ // packageName: "analytics",
942
+ // modelPath: "orders.malloy",
943
+ // versionId: "abc123"
944
+ // }
945
+ ```
946
+
947
+ ---
948
+
949
+ ### ParsedResource Type
950
+
951
+ ```typescript
952
+ type ParsedResource = {
953
+ projectName: string;
954
+ packageName?: string;
955
+ connectionName?: string;
956
+ versionId?: string;
957
+ modelPath?: string;
958
+ };
959
+ ```
960
+
961
+ ---
962
+
963
+ ## Workbook Storage
964
+
965
+ Workbooks are interactive analysis documents that can be saved and loaded. The SDK provides a storage abstraction that you can implement for different backends.
966
+
967
+ ### WorkbookStorage Interface
968
+
969
+ ```typescript
970
+ interface Workspace {
971
+ name: string;
972
+ writeable: boolean;
973
+ description: string;
974
+ }
975
+
976
+ interface WorkbookLocator {
977
+ path: string;
978
+ workspace: string;
979
+ }
980
+
981
+ interface WorkbookStorage {
982
+ listWorkspaces(writeableOnly: boolean): Promise<Workspace[]>;
983
+ listWorkbooks(workspace: Workspace): Promise<WorkbookLocator[]>;
984
+ getWorkbook(path: WorkbookLocator): Promise<string>;
985
+ deleteWorkbook(path: WorkbookLocator): Promise<void>;
986
+ saveWorkbook(path: WorkbookLocator, workbook: string): Promise<void>;
987
+ moveWorkbook(from: WorkbookLocator, to: WorkbookLocator): Promise<void>;
988
+ }
989
+ ```
990
+
991
+ ---
992
+
993
+ ### BrowserWorkbookStorage
994
+
995
+ Built-in implementation using browser localStorage.
996
+
997
+ ```tsx
998
+ import {
999
+ BrowserWorkbookStorage,
1000
+ WorkbookStorageProvider,
1001
+ } from "@malloy-publisher/sdk";
1002
+
1003
+ const storage = new BrowserWorkbookStorage();
1004
+
1005
+ <WorkbookStorageProvider workbookStorage={storage}>
1006
+ <App />
1007
+ </WorkbookStorageProvider>;
1008
+ ```
1009
+
1010
+ ---
1011
+
1012
+ ### Custom Storage Implementation
1013
+
1014
+ ```tsx
1015
+ class S3WorkbookStorage implements WorkbookStorage {
1016
+ private s3Client: S3Client;
1017
+ private bucket: string;
1018
+
1019
+ constructor(s3Client: S3Client, bucket: string) {
1020
+ this.s3Client = s3Client;
1021
+ this.bucket = bucket;
1022
+ }
1023
+
1024
+ async listWorkspaces(writeableOnly: boolean): Promise<Workspace[]> {
1025
+ return [
1026
+ {
1027
+ name: this.bucket,
1028
+ writeable: true,
1029
+ description: "S3 bucket storage",
1030
+ },
1031
+ ];
1032
+ }
1033
+
1034
+ async listWorkbooks(workspace: Workspace): Promise<WorkbookLocator[]> {
1035
+ const objects = await this.s3Client.listObjects(
1036
+ this.bucket,
1037
+ "workbooks/",
1038
+ );
1039
+ return objects.map((obj) => ({
1040
+ path: obj.key,
1041
+ workspace: workspace.name,
1042
+ }));
1043
+ }
1044
+
1045
+ async getWorkbook(path: WorkbookLocator): Promise<string> {
1046
+ const data = await this.s3Client.getObject(this.bucket, path.path);
1047
+ return data.toString();
1048
+ }
1049
+
1050
+ async saveWorkbook(path: WorkbookLocator, workbook: string): Promise<void> {
1051
+ await this.s3Client.putObject(this.bucket, path.path, workbook);
1052
+ }
1053
+
1054
+ async deleteWorkbook(path: WorkbookLocator): Promise<void> {
1055
+ await this.s3Client.deleteObject(this.bucket, path.path);
1056
+ }
1057
+
1058
+ async moveWorkbook(
1059
+ from: WorkbookLocator,
1060
+ to: WorkbookLocator,
1061
+ ): Promise<void> {
1062
+ const content = await this.getWorkbook(from);
1063
+ await this.saveWorkbook(to, content);
1064
+ await this.deleteWorkbook(from);
1065
+ }
1066
+ }
1067
+
1068
+ // Usage
1069
+ const storage = new S3WorkbookStorage(s3Client, "my-workbooks-bucket");
1070
+
1071
+ <WorkbookStorageProvider workbookStorage={storage}>
1072
+ <App />
1073
+ </WorkbookStorageProvider>;
1074
+ ```
1075
+
1076
+ ---
1077
+
1078
+ ### WorkbookStorageProvider
1079
+
1080
+ Context provider for workbook storage.
1081
+
1082
+ ```tsx
1083
+ import {
1084
+ WorkbookStorageProvider,
1085
+ useWorkbookStorage,
1086
+ } from "@malloy-publisher/sdk";
1087
+
1088
+ // Provider setup
1089
+ <WorkbookStorageProvider workbookStorage={myStorage}>
1090
+ <App />
1091
+ </WorkbookStorageProvider>;
1092
+
1093
+ // Access in components
1094
+ function MyComponent() {
1095
+ const { workbookStorage } = useWorkbookStorage();
1096
+
1097
+ const workbooks = await workbookStorage.listWorkbooks({
1098
+ name: "Local",
1099
+ writeable: true,
1100
+ description: "",
1101
+ });
1102
+ }
1103
+ ```
1104
+
1105
+ ---
1106
+
1107
+ ## Styling
1108
+
1109
+ ### Required CSS
1110
+
1111
+ Import the SDK styles in your app entry point:
1112
+
1113
+ ```tsx
1114
+ // Main SDK styles (required)
1115
+ import "@malloy-publisher/sdk/styles.css";
1116
+
1117
+ // If using Model/ModelExplorer outside of Publisher
1118
+ import "@malloy-publisher/sdk/malloy-explorer.css";
1119
+
1120
+ // If using Workbook markdown editor
1121
+ import "@malloy-publisher/sdk/markdown-editor.css";
1122
+ ```
1123
+
1124
+ ---
1125
+
1126
+ ### Material-UI Theme
1127
+
1128
+ The SDK uses Material-UI (MUI) v7. You can customize the theme:
1129
+
1130
+ ```tsx
1131
+ import { createTheme, ThemeProvider, CssBaseline } from "@mui/material";
1132
+ import { ServerProvider } from "@malloy-publisher/sdk";
1133
+
1134
+ const theme = createTheme({
1135
+ palette: {
1136
+ primary: {
1137
+ main: "#14b3cb", // Malloy teal
1138
+ },
1139
+ secondary: {
1140
+ main: "#fbbb04", // Malloy yellow
1141
+ },
1142
+ },
1143
+ typography: {
1144
+ fontFamily: '"Inter", "Roboto", sans-serif',
1145
+ },
1146
+ });
1147
+
1148
+ function App() {
1149
+ return (
1150
+ <ServerProvider>
1151
+ <ThemeProvider theme={theme}>
1152
+ <CssBaseline />
1153
+ {/* Your app */}
1154
+ </ThemeProvider>
1155
+ </ServerProvider>
1156
+ );
1157
+ }
1158
+ ```
1159
+
1160
+ ---
1161
+
1162
+ ### Styled Components
1163
+
1164
+ The SDK exports several pre-styled components for consistent UI:
1165
+
1166
+ ```tsx
1167
+ import {
1168
+ StyledCard,
1169
+ StyledCardContent,
1170
+ StyledCardMedia,
1171
+ PackageCard,
1172
+ PackageCardContent,
1173
+ PackageSectionTitle,
1174
+ CleanNotebookContainer,
1175
+ CleanNotebookSection,
1176
+ } from "@malloy-publisher/sdk";
1177
+ ```
1178
+
1179
+ ---
1180
+
1181
+ ## Building a Custom Data App
1182
+
1183
+ ### Example: Dashboard with Multiple Visualizations
1184
+
1185
+ ```tsx
1186
+ import {
1187
+ ServerProvider,
1188
+ QueryResult,
1189
+ useModelData,
1190
+ encodeResourceUri,
1191
+ ApiErrorDisplay,
1192
+ Loading,
1193
+ } from "@malloy-publisher/sdk";
1194
+ import "@malloy-publisher/sdk/styles.css";
1195
+ import { Grid, Typography, Paper } from "@mui/material";
1196
+
1197
+ function Dashboard() {
1198
+ const resourceUri = encodeResourceUri({
1199
+ projectName: "my-project",
1200
+ packageName: "analytics",
1201
+ modelPath: "models/sales.malloy",
1202
+ });
1203
+
1204
+ const { data, isLoading, isError, error } = useModelData(resourceUri);
1205
+
1206
+ if (isLoading) return <Loading text="Loading dashboard..." />;
1207
+ if (isError) return <ApiErrorDisplay error={error} context="Dashboard" />;
1208
+
1209
+ return (
1210
+ <Grid container spacing={3}>
1211
+ <Grid item xs={12}>
1212
+ <Typography variant="h4">Sales Dashboard</Typography>
1213
+ </Grid>
1214
+
1215
+ <Grid item xs={12} md={6}>
1216
+ <Paper sx={{ p: 2 }}>
1217
+ <Typography variant="h6">Sales by Region</Typography>
1218
+ <QueryResult
1219
+ sourceName="orders"
1220
+ queryName="by_region"
1221
+ resourceUri={resourceUri}
1222
+ />
1223
+ </Paper>
1224
+ </Grid>
1225
+
1226
+ <Grid item xs={12} md={6}>
1227
+ <Paper sx={{ p: 2 }}>
1228
+ <Typography variant="h6">Monthly Trends</Typography>
1229
+ <QueryResult
1230
+ sourceName="orders"
1231
+ queryName="monthly_trends"
1232
+ resourceUri={resourceUri}
1233
+ />
1234
+ </Paper>
1235
+ </Grid>
1236
+
1237
+ <Grid item xs={12}>
1238
+ <Paper sx={{ p: 2 }}>
1239
+ <Typography variant="h6">Custom Query</Typography>
1240
+ <QueryResult
1241
+ query="run: orders -> {
1242
+ group_by: product_category
1243
+ aggregate:
1244
+ total_revenue is sum(revenue)
1245
+ avg_order_value is avg(order_value)
1246
+ }"
1247
+ resourceUri={resourceUri}
1248
+ />
1249
+ </Paper>
1250
+ </Grid>
1251
+ </Grid>
1252
+ );
1253
+ }
1254
+
1255
+ function App() {
1256
+ return (
1257
+ <ServerProvider baseURL="http://localhost:4000/api/v0">
1258
+ <Dashboard />
1259
+ </ServerProvider>
1260
+ );
1261
+ }
1262
+ ```
1263
+
1264
+ ---
1265
+
1266
+ ### Example: Data Table with Raw Query Data
1267
+
1268
+ ```tsx
1269
+ import {
1270
+ ServerProvider,
1271
+ useRawQueryData,
1272
+ encodeResourceUri,
1273
+ Loading,
1274
+ ApiErrorDisplay,
1275
+ } from "@malloy-publisher/sdk";
1276
+ import { DataGrid } from "@mui/x-data-grid";
1277
+
1278
+ function DataTable() {
1279
+ const resourceUri = encodeResourceUri({
1280
+ projectName: "my-project",
1281
+ packageName: "analytics",
1282
+ modelPath: "models/customers.malloy",
1283
+ });
1284
+
1285
+ const { data, isLoading, isError, error } = useRawQueryData({
1286
+ resourceUri,
1287
+ modelPath: "models/customers.malloy",
1288
+ sourceName: "customers",
1289
+ queryName: "all_customers",
1290
+ });
1291
+
1292
+ if (isLoading) return <Loading text="Loading data..." />;
1293
+ if (isError) return <ApiErrorDisplay error={error} />;
1294
+
1295
+ const columns =
1296
+ data.length > 0
1297
+ ? Object.keys(data[0]).map((key) => ({
1298
+ field: key,
1299
+ headerName: key,
1300
+ width: 150,
1301
+ }))
1302
+ : [];
1303
+
1304
+ return (
1305
+ <DataGrid
1306
+ rows={data.map((row, i) => ({ id: i, ...row }))}
1307
+ columns={columns}
1308
+ pageSize={10}
1309
+ autoHeight
1310
+ />
1311
+ );
1312
+ }
1313
+ ```
1314
+
1315
+ ---
1316
+
1317
+ ### Example: Interactive Model Explorer
1318
+
1319
+ ```tsx
1320
+ import {
1321
+ ServerProvider,
1322
+ ModelExplorer,
1323
+ encodeResourceUri,
1324
+ } from "@malloy-publisher/sdk";
1325
+ import "@malloy-publisher/sdk/styles.css";
1326
+ import "@malloy-publisher/sdk/malloy-explorer.css";
1327
+ import { useState } from "react";
1328
+
1329
+ function Explorer() {
1330
+ const [selectedQuery, setSelectedQuery] = useState(null);
1331
+
1332
+ const resourceUri = encodeResourceUri({
1333
+ projectName: "my-project",
1334
+ packageName: "analytics",
1335
+ modelPath: "models/orders.malloy",
1336
+ });
1337
+
1338
+ return (
1339
+ <div style={{ display: "flex", gap: "20px" }}>
1340
+ <div style={{ flex: 1 }}>
1341
+ <h2>Build Your Query</h2>
1342
+ <ModelExplorer
1343
+ resourceUri={resourceUri}
1344
+ onChange={(result) => {
1345
+ setSelectedQuery(result);
1346
+ console.log("Generated Query:", result.query);
1347
+ }}
1348
+ />
1349
+ </div>
1350
+
1351
+ {selectedQuery && (
1352
+ <div style={{ flex: 1 }}>
1353
+ <h2>Query Preview</h2>
1354
+ <pre>{selectedQuery.query}</pre>
1355
+ </div>
1356
+ )}
1357
+ </div>
1358
+ );
1359
+ }
1360
+ ```
1361
+
1362
+ ---
1363
+
1364
+ ### Example: Lightweight Client-Only Setup
1365
+
1366
+ For minimal bundle size when you only need API access:
1367
+
1368
+ ```tsx
1369
+ // Use the client entry point
1370
+ import { ServerProvider, useServer } from "@malloy-publisher/sdk/client";
1371
+
1372
+ function MyApp() {
1373
+ return (
1374
+ <ServerProvider baseURL="http://localhost:4000/api/v0">
1375
+ <ProjectList />
1376
+ </ServerProvider>
1377
+ );
1378
+ }
1379
+
1380
+ function ProjectList() {
1381
+ const { apiClients } = useServer();
1382
+ const [projects, setProjects] = useState([]);
1383
+
1384
+ useEffect(() => {
1385
+ apiClients.projects
1386
+ .listProjects()
1387
+ .then((response) => setProjects(response.data));
1388
+ }, []);
1389
+
1390
+ return (
1391
+ <ul>
1392
+ {projects.map((p) => (
1393
+ <li key={p.name}>{p.name}</li>
1394
+ ))}
1395
+ </ul>
1396
+ );
1397
+ }
1398
+ ```
1399
+
1400
+ ---
1401
+
1402
+ ## API Reference
1403
+
1404
+ ### Exported Components
1405
+
1406
+ | Component | Description |
1407
+ | ------------------------- | ----------------------------------------------- |
1408
+ | `ServerProvider` | Required context provider for API access |
1409
+ | `Home` | Project listing landing page |
1410
+ | `Project` | Package listing for a project |
1411
+ | `Package` | Package detail (models, notebooks, connections) |
1412
+ | `Model` | Full model explorer with visual query builder |
1413
+ | `ModelExplorer` | Lower-level query builder component |
1414
+ | `ModelExplorerDialog` | Model explorer in a modal dialog |
1415
+ | `Notebook` | Read-only notebook viewer |
1416
+ | `Workbook` | Interactive workbook editor |
1417
+ | `WorkbookList` | List workbooks from storage |
1418
+ | `WorkbookManager` | Workbook state management class |
1419
+ | `WorkbookStorageProvider` | Context for workbook storage |
1420
+ | `QueryResult` | Execute and display query |
1421
+ | `RenderedResult` | Render Malloy result JSON |
1422
+ | `EmbeddedQueryResult` | Render serialized query config |
1423
+ | `Loading` | Loading spinner with text |
1424
+ | `ApiErrorDisplay` | Error display component |
1425
+ | `AnalyzePackageButton` | Create/manage workbooks |
1426
+ | `SourcesExplorer` | Source schema browser |
1427
+ | `ConnectionExplorer` | Connection management UI |
1428
+
1429
+ ### Exported Hooks
1430
+
1431
+ | Hook | Description |
1432
+ | ------------------------------- | ----------------------------------- |
1433
+ | `useServer` | Access ServerProvider context |
1434
+ | `useQueryWithApiError` | React Query with error handling |
1435
+ | `useMutationWithApiError` | Mutations with error handling |
1436
+ | `useModelData` | Fetch compiled model |
1437
+ | `useRawQueryData` | Execute query, get raw data |
1438
+ | `useRouterClickHandler` | Smart navigation with modifier keys |
1439
+ | `useWorkbookStorage` | Access workbook storage context |
1440
+ | `useDimensionFiltersFromSpec` | Programmatic dimensional filtering |
1441
+
1442
+ ### Exported Utilities
1443
+
1444
+ | Utility | Description |
1445
+ | --------------------------- | ----------------------------------- |
1446
+ | `encodeResourceUri` | Create resource URI from components |
1447
+ | `parseResourceUri` | Parse resource URI to components |
1448
+ | `createEmbeddedQueryResult` | Serialize query config |
1449
+ | `BrowserWorkbookStorage` | localStorage-based workbook storage |
1450
+ | `globalQueryClient` | Shared React Query client |
1451
+
1452
+ ### Exported Types
1453
+
1454
+ | Type | Description |
1455
+ | ------------------------ | ------------------------------------ |
1456
+ | `ParsedResource` | Parsed resource URI components |
1457
+ | `ServerContextValue` | Server context interface |
1458
+ | `ServerProviderProps` | ServerProvider props |
1459
+ | `QueryExplorerResult` | Query builder result |
1460
+ | `SourceAndPath` | Source info with model path |
1461
+ | `WorkbookStorage` | Workbook storage interface |
1462
+ | `WorkbookLocator` | Workbook path + workspace |
1463
+ | `Workspace` | Workspace metadata |
1464
+ | `ApiError` | Standardized API error |
1465
+ | `ModelExplorerProps` | ModelExplorer props |
1466
+ | `DimensionFiltersConfig` | Dimensional filter configuration |
1467
+
1468
+ ---
1469
+
1470
+ ## Additional Resources
1471
+
1472
+ - [Publisher GitHub Repository](https://github.com/malloydata/publisher)
1473
+ - [Malloy Language Reference](https://docs.malloydata.dev/)
1474
+ - [Malloy Slack Community](https://join.slack.com/t/malloy-community/shared_invite/zt-1kgfwgi5g-CrsdaRqs81QY67QW0~t_uw)