@lightdash/query-sdk 1.143.0 → 1.144.1

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.
@@ -10,6 +10,9 @@ import { type LightdashClient } from './client';
10
10
  import type { Transport } from './types';
11
11
  export declare function useTransport(): Transport;
12
12
  export declare function useLightdashClient(): LightdashClient | null;
13
+ /** Non-throwing transport lookup for hooks that must keep working without a
14
+ * provider (`useVizContext` supports standalone self-subscription). */
15
+ export declare function useOptionalTransport(): Transport | null;
13
16
  type LightdashProviderProps = {
14
17
  children: ReactNode;
15
18
  } & ({
@@ -20,6 +20,12 @@ export function useLightdashClient() {
20
20
  const ctx = useContext(LightdashContext);
21
21
  return ctx?.client ?? null;
22
22
  }
23
+ /** Non-throwing transport lookup for hooks that must keep working without a
24
+ * provider (`useVizContext` supports standalone self-subscription). */
25
+ export function useOptionalTransport() {
26
+ const ctx = useContext(LightdashContext);
27
+ return ctx?.transport ?? null;
28
+ }
23
29
  export function LightdashProvider({ children, ...props }) {
24
30
  const client = ('client' in props ? props.client : null) ?? null;
25
31
  const transport = client?.transport ??
@@ -7,6 +7,7 @@
7
7
  * 2. Poll GET /api/v2/projects/{projectUuid}/query/{queryUuid}
8
8
  * → returns results when status is 'ready'
9
9
  */
10
+ import { VIZ_UNDERLYING_DATA_PATH } from './types';
10
11
  // Mirrors the explorer's `useInfiniteQueryResults` polling rhythm so the
11
12
  // SDK behaves like a normal Lightdash chart: 500-row pages, exponential
12
13
  // backoff starting at 250ms, capped at 1000ms.
@@ -653,5 +654,36 @@ export function createApiTransport(config, adapter) {
653
654
  // to resolve. Only the in-iframe postMessage transport supports it.
654
655
  throw new Error('externalFetch is only available inside a data app preview');
655
656
  },
657
+ async getVizUnderlyingData(intent) {
658
+ const execResult = await fetchFn('POST', VIZ_UNDERLYING_DATA_PATH, intent);
659
+ const { firstReadyPage, apiRows } = await pollQueryRows(fetchFn, config.projectUuid, execResult.queryUuid);
660
+ const fieldIds = [
661
+ ...Object.keys(execResult.fields),
662
+ ...Object.keys(firstReadyPage.columns).filter((fieldId) => !(fieldId in execResult.fields)),
663
+ ];
664
+ return {
665
+ ...mapApiRowsToQueryResult({
666
+ apiRows,
667
+ columns: firstReadyPage.columns,
668
+ fields: execResult.fields,
669
+ fieldIds,
670
+ fieldNameForId: (fieldId) => fieldId,
671
+ }),
672
+ queryUuid: execResult.queryUuid,
673
+ };
674
+ },
675
+ async downloadVizUnderlyingData(intent, options = {}) {
676
+ const execResult = await fetchFn('POST', VIZ_UNDERLYING_DATA_PATH, {
677
+ ...intent,
678
+ limit: getUnderlyingDownloadLimit(options.limit),
679
+ });
680
+ await pollQueryReady(fetchFn, config.projectUuid, execResult.queryUuid, 1);
681
+ return scheduleDownloadForQuery({
682
+ fetchFn,
683
+ projectUuid: config.projectUuid,
684
+ queryUuid: execResult.queryUuid,
685
+ options,
686
+ });
687
+ },
656
688
  };
657
689
  }
package/dist/features.js CHANGED
@@ -83,6 +83,12 @@ export const SDK_FEATURES = [
83
83
  description: "Scheduled deliveries and their preview render every tab or slide's data, not just the one currently visible.",
84
84
  wiring: "Gate tab/slide content so DATA components for all tabs mount when useDeliveryRender() is true — tabs switch what's shown, not what's fetched. Never mount all tabs unconditionally.",
85
85
  },
86
+ {
87
+ key: 'viz-underlying-data',
88
+ label: 'View underlying data',
89
+ description: 'Open the raw result rows behind a clicked data point in a reusable visualization, with CSV/XLSX download.',
90
+ wiring: 'In the viz, keep the untransformed source row on each interactive datum, show a data-point action menu only when useVizContext().underlyingData.enabled and the mark maps to exactly one source row, render underlyingData.get({ row, metric }) in a themed dialog, and wire its Download button to underlyingData.download.',
91
+ },
86
92
  ];
87
93
  export const SDK_FEATURE_KEYS = SDK_FEATURES.map((f) => f.key);
88
94
  export const SDK_MANIFEST_MESSAGE_TYPE = 'lightdash:sdk:manifest';
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.143.0";
1
+ export declare const SDK_VERSION = "1.144.1";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/generateSdkVersion.mjs (prebuild) — do not edit.
2
- export const SDK_VERSION = '1.143.0';
2
+ export const SDK_VERSION = '1.144.1';
package/dist/index.d.ts CHANGED
@@ -15,7 +15,7 @@ export type { DeliveryQuery } from './delivery';
15
15
  export { exportToSheets } from './exportToSheets';
16
16
  export type { ExportToSheetsOptions, ExportToSheetsResult, } from './exportToSheets';
17
17
  export { VizContextProvider, useVizContext, getFormatted, getRaw, } from './vizContext';
18
- export type { VizContext, VizContextCell, VizContextOptionValue, VizContextRow, DataAppVizContextMessage, VizContextRequestMessage, } from './vizContext';
18
+ export type { VizContext, VizContextCell, VizContextOptionValue, VizContextRow, VizUnderlyingData, DataAppVizContextMessage, VizContextRequestMessage, } from './vizContext';
19
19
  export { useColorScheme } from './colorScheme';
20
20
  export type { HostColorScheme, HostColorSchemeMessage, HostColorSchemeRequestMessage, } from './colorScheme';
21
21
  export { isDeliveryRender, useDeliveryRender } from './deliveryRender';
package/dist/types.d.ts CHANGED
@@ -162,6 +162,29 @@ export type UnderlyingDataResult = {
162
162
  format: FormatFunction;
163
163
  queryUuid: string;
164
164
  };
165
+ /**
166
+ * Bridge-only virtual route for viz underlying-data click intents. Duplicated
167
+ * from `@lightdash/common` (`APP_SDK_VIZ_UNDERLYING_DATA_PATH`) — this package
168
+ * must not depend on common. The path only resolves behind the host's
169
+ * postMessage bridge, which rewrites it into the real underlying-data request;
170
+ * on a direct-API transport it fails with a plain HTTP error.
171
+ */
172
+ export declare const VIZ_UNDERLYING_DATA_PATH = "/__sdk/viz/underlying-data";
173
+ /**
174
+ * Semantic click intent a viz sends to the host: the untransformed source row
175
+ * (as received from `useVizContext().rows`) and the declared field NAME bound
176
+ * to the clicked metric slot. The host resolves everything else.
177
+ */
178
+ export type VizUnderlyingDataIntent = {
179
+ row: Record<string, {
180
+ value?: {
181
+ raw?: unknown;
182
+ formatted?: string;
183
+ };
184
+ } | undefined>;
185
+ metric: string;
186
+ limit?: number | null;
187
+ };
165
188
  export type LightdashClientConfig = {
166
189
  /** Lightdash instance URL */
167
190
  baseUrl: string;
@@ -223,4 +246,12 @@ export type Transport = {
223
246
  }) => Promise<QueryResult>;
224
247
  getUser: () => Promise<LightdashUser>;
225
248
  externalFetch: (alias: string, opts: ExternalFetchOptions) => Promise<ExternalFetchResult>;
249
+ /**
250
+ * Fetch the raw rows behind a viz data point via the host bridge.
251
+ * Optional so custom transports predating the capability stay valid —
252
+ * `useVizContext().underlyingData.enabled` is false when absent.
253
+ */
254
+ getVizUnderlyingData?: (intent: VizUnderlyingDataIntent) => Promise<UnderlyingDataResult>;
255
+ /** Schedule a CSV/XLSX export of the rows behind a viz data point. */
256
+ downloadVizUnderlyingData?: (intent: Omit<VizUnderlyingDataIntent, 'limit'>, options?: DownloadResultsOptions) => Promise<DownloadResultsResult>;
226
257
  };
package/dist/types.js CHANGED
@@ -1,4 +1,12 @@
1
1
  /**
2
2
  * Core types for the Lightdash SDK.
3
3
  */
4
- export {};
4
+ // --- Viz underlying data ---
5
+ /**
6
+ * Bridge-only virtual route for viz underlying-data click intents. Duplicated
7
+ * from `@lightdash/common` (`APP_SDK_VIZ_UNDERLYING_DATA_PATH`) — this package
8
+ * must not depend on common. The path only resolves behind the host's
9
+ * postMessage bridge, which rewrites it into the real underlying-data request;
10
+ * on a direct-API transport it fails with a plain HTTP error.
11
+ */
12
+ export const VIZ_UNDERLYING_DATA_PATH = '/__sdk/viz/underlying-data';
@@ -14,6 +14,7 @@
14
14
  * host's reply therefore can't be missed. No timers, no races.
15
15
  */
16
16
  import { type ReactNode } from 'react';
17
+ import type { DownloadResultsOptions, DownloadResultsResult, Transport, UnderlyingDataResult } from './types';
17
18
  /** A single cell of a Lightdash result row: `{ value: { raw, formatted } }`. */
18
19
  export type VizContextCell = {
19
20
  value?: {
@@ -45,6 +46,10 @@ export type DataAppVizContextMessage = {
45
46
  options?: Record<string, VizContextOptionValue>;
46
47
  /** Absent when the installed host predates palette delivery. */
47
48
  colorPalette?: string[];
49
+ /** Absent when the installed host predates underlying-data delivery. */
50
+ underlyingData?: {
51
+ enabled?: boolean;
52
+ };
48
53
  };
49
54
  /** Posted by the iframe on mount so the host pushes the current context. */
50
55
  export type VizContextRequestMessage = {
@@ -54,6 +59,25 @@ export type VizContextRequestMessage = {
54
59
  export declare const getFormatted: (row: VizContextRow | undefined, fieldId: string | undefined) => string;
55
60
  /** Raw value for a field's cell in a row (number/string/etc.), or null when unset. */
56
61
  export declare const getRaw: (row: VizContextRow | undefined, fieldId: string | undefined) => unknown;
62
+ /**
63
+ * Host-mediated access to the raw rows behind a clicked data point. `enabled`
64
+ * is false when the host predates the capability, the viewer lacks permission,
65
+ * or no transport is mounted — render no menu item in that case (never a
66
+ * disabled one). `row` is the untransformed source row from `rows`; `metric`
67
+ * is the declared field NAME bound to the clicked metric slot.
68
+ */
69
+ export type VizUnderlyingData = {
70
+ enabled: boolean;
71
+ get: (opts: {
72
+ row: VizContextRow;
73
+ metric: string;
74
+ limit?: number;
75
+ }) => Promise<UnderlyingDataResult>;
76
+ download: (opts: {
77
+ row: VizContextRow;
78
+ metric: string;
79
+ } & DownloadResultsOptions) => Promise<DownloadResultsResult>;
80
+ };
57
81
  export type VizContext = {
58
82
  /** field name → query field id, as bound in the host field mapping UI. */
59
83
  fieldMapping: Record<string, string>;
@@ -70,12 +94,15 @@ export type VizContext = {
70
94
  colorPalette: string[];
71
95
  /** False until the first context arrives — render a placeholder while false. */
72
96
  ready: boolean;
97
+ /** Fetch/export the raw rows behind a clicked data point via the host. */
98
+ underlyingData: VizUnderlyingData;
73
99
  };
74
100
  type VizContextValue = {
75
101
  fieldMapping: Record<string, string>;
76
102
  rows: VizContextRow[];
77
103
  options: Record<string, VizContextOptionValue>;
78
104
  colorPalette: string[];
105
+ underlyingDataEnabled: boolean;
79
106
  };
80
107
  type VizContextState = VizContextValue | null;
81
108
  /**
@@ -85,6 +112,12 @@ type VizContextState = VizContextValue | null;
85
112
  * `{}` / `[]`.
86
113
  */
87
114
  export declare function toVizContextState(message: DataAppVizContextMessage): VizContextValue;
115
+ /**
116
+ * Builds the `underlyingData` surface from the host's availability flag and
117
+ * the mounted transport (null when no `LightdashProvider` is present, e.g.
118
+ * standalone `useVizContext` usage). Exported for tests.
119
+ */
120
+ export declare function buildVizUnderlyingData(hostEnabled: boolean, transport: Transport | null): VizUnderlyingData;
88
121
  declare const NO_PROVIDER: unique symbol;
89
122
  /**
90
123
  * Owns the single listener + handshake for a data app viz. Mount it in the
@@ -13,7 +13,8 @@
13
13
  * request is guaranteed to be sent after any listener the app registered. The
14
14
  * host's reply therefore can't be missed. No timers, no races.
15
15
  */
16
- import { createContext, createElement, useContext, useEffect, useState, } from 'react';
16
+ import { createContext, createElement, useContext, useEffect, useMemo, useState, } from 'react';
17
+ import { useOptionalTransport } from './LightdashProvider';
17
18
  const DATA_APP_VIZ_CONTEXT_MESSAGE = 'lightdash:sdk:data-app-viz-context';
18
19
  const VIZ_CONTEXT_REQUEST_MESSAGE = 'lightdash:sdk:viz-context-request';
19
20
  /** Display string for a field's cell in a row, e.g. `"$1,234"`. Empty when unset. */
@@ -53,6 +54,40 @@ export function toVizContextState(message) {
53
54
  colorPalette: Array.isArray(message.colorPalette)
54
55
  ? message.colorPalette.filter((color) => typeof color === 'string')
55
56
  : [],
57
+ // Strict boolean check — non-boolean payloads read as disabled.
58
+ underlyingDataEnabled: message.underlyingData?.enabled === true,
59
+ };
60
+ }
61
+ /**
62
+ * Builds the `underlyingData` surface from the host's availability flag and
63
+ * the mounted transport (null when no `LightdashProvider` is present, e.g.
64
+ * standalone `useVizContext` usage). Exported for tests.
65
+ */
66
+ export function buildVizUnderlyingData(hostEnabled, transport) {
67
+ // Atomic capability: the generated menu promises Download whenever
68
+ // `enabled` is true, so a transport must implement both methods.
69
+ const supported = typeof transport?.getVizUnderlyingData === 'function' &&
70
+ typeof transport?.downloadVizUnderlyingData === 'function';
71
+ return {
72
+ enabled: hostEnabled && supported,
73
+ get: async ({ row, metric, limit }) => {
74
+ if (!hostEnabled) {
75
+ throw new Error('Underlying data is not enabled for this visualization.');
76
+ }
77
+ if (!transport?.getVizUnderlyingData) {
78
+ throw new Error('This SDK build predates underlying data. Rebuild the app on the current template.');
79
+ }
80
+ return transport.getVizUnderlyingData({ row, metric, limit });
81
+ },
82
+ download: async ({ row, metric, ...options }) => {
83
+ if (!hostEnabled) {
84
+ throw new Error('Underlying data is not enabled for this visualization.');
85
+ }
86
+ if (!transport?.downloadVizUnderlyingData) {
87
+ throw new Error('This SDK build predates underlying data. Rebuild the app on the current template.');
88
+ }
89
+ return transport.downloadVizUnderlyingData({ row, metric }, options);
90
+ },
56
91
  };
57
92
  }
58
93
  // Distinguishes "no provider mounted" from "provider present, no context yet".
@@ -113,11 +148,17 @@ export function useVizContext() {
113
148
  const context = hasProvider
114
149
  ? fromProvider
115
150
  : selfSubscribed;
151
+ // Null-returning lookup — standalone usage without LightdashProvider keeps
152
+ // working, with underlying data reported as unavailable.
153
+ const transport = useOptionalTransport();
154
+ const hostEnabled = context?.underlyingDataEnabled === true;
155
+ const underlyingData = useMemo(() => buildVizUnderlyingData(hostEnabled, transport), [hostEnabled, transport]);
116
156
  return {
117
157
  fieldMapping: context?.fieldMapping ?? {},
118
158
  rows: context?.rows ?? [],
119
159
  options: context?.options ?? {},
120
160
  colorPalette: context?.colorPalette ?? [],
121
161
  ready: context !== null,
162
+ underlyingData,
122
163
  };
123
164
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/query-sdk",
3
- "version": "1.143.0",
3
+ "version": "1.144.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "SDK for building custom data apps against the Lightdash semantic layer",
@@ -34,7 +34,7 @@
34
34
  "jsdom": "26.1.0",
35
35
  "typescript": "7.0.2",
36
36
  "vitest": "4.1.6",
37
- "@lightdash/common": "1.143.0"
37
+ "@lightdash/common": "1.144.1"
38
38
  },
39
39
  "scripts": {
40
40
  "prebuild": "node ./scripts/generateSdkVersion.mjs",