@lightdash/query-sdk 3.89.0 → 3.91.0

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.
@@ -489,10 +489,18 @@ function mapApiRowsToQueryResult({ apiRows, columns, fields, fieldIds, fieldName
489
489
  const name = idToName.get(fieldId) ?? fieldId;
490
490
  const fieldMeta = fields[fieldId];
491
491
  const colMeta = columns[fieldId];
492
+ const urls = Array.isArray(fieldMeta?.urls)
493
+ ? fieldMeta.urls.filter((u) => typeof u === 'object' &&
494
+ u !== null &&
495
+ typeof u.label === 'string' &&
496
+ typeof u.url === 'string')
497
+ : [];
492
498
  return {
493
499
  name,
494
500
  label: fieldMeta?.label ?? fieldMeta?.displayName ?? name,
495
501
  type: mapColumnType(colMeta?.type ?? fieldMeta?.type ?? 'string'),
502
+ fieldId,
503
+ ...(urls.length > 0 ? { urls } : {}),
496
504
  };
497
505
  });
498
506
  const formattedCache = new Map();
@@ -0,0 +1,22 @@
1
+ import type { Column, FormatFunction, Row } from './types';
2
+ export type ResolvedColumnUrl = {
3
+ label: string;
4
+ href: string;
5
+ };
6
+ type ResolveArgs = {
7
+ /** The column whose cell is being rendered. */
8
+ column: Column;
9
+ /** The result row the cell belongs to (SDK row, keyed by column name). */
10
+ row: Row;
11
+ /** All result columns — used to resolve `${row.<table>.<field>}` references. */
12
+ columns: Column[];
13
+ /** The result's format function, for `.formatted` references. */
14
+ format?: FormatFunction;
15
+ };
16
+ /**
17
+ * Build the links a cell offers from its column's `urls` patterns. Patterns
18
+ * whose references cannot be resolved against this row (missing field, null
19
+ * value, unknown filter) are skipped, matching dashboard behavior.
20
+ */
21
+ export declare const resolveColumnUrls: (args: ResolveArgs) => ResolvedColumnUrl[];
22
+ export {};
@@ -0,0 +1,88 @@
1
+ const TEMPLATE_RE = /\$\{([^}]+)\}/g;
2
+ /**
3
+ * Find the row key a `${row.<table>.<field>}` reference points at. Column
4
+ * names differ per result path (plain names, dot notation, or qualified ids
5
+ * in underlying-data results), so resolve through the column's `fieldId`
6
+ * (`<table>_<field>`), which preserves table identity everywhere. A dotted
7
+ * name match remains as a fallback for hand-built columns without a fieldId;
8
+ * a bare field name is never matched — the same name on another table would
9
+ * silently link the wrong record.
10
+ */
11
+ const rowKeyForReference = (columns, table, field) => {
12
+ const fieldId = `${table}_${field}`;
13
+ const byFieldId = columns.find((c) => c.fieldId === fieldId);
14
+ if (byFieldId)
15
+ return byFieldId.name;
16
+ const dotted = `${table}.${field}`;
17
+ const byDottedName = columns.find((c) => c.fieldId === undefined && c.name === dotted);
18
+ return byDottedName ? byDottedName.name : null;
19
+ };
20
+ const applyFilters = (value, filters) => {
21
+ let out = value;
22
+ for (const filter of filters) {
23
+ if (filter === 'url_encode') {
24
+ out = encodeURIComponent(out);
25
+ }
26
+ else {
27
+ // Unknown filter — mirror the dashboard renderer's strict mode
28
+ // and drop the link rather than emit a wrong URL.
29
+ return null;
30
+ }
31
+ }
32
+ return out;
33
+ };
34
+ /**
35
+ * Resolve one `${...}` expression against the row. Returns null when the
36
+ * reference is invalid, the referenced value is missing, or a filter is
37
+ * unknown — the caller then skips the whole pattern.
38
+ */
39
+ const resolveExpression = (expression, { column, row, columns, format }) => {
40
+ const [ref = '', ...filters] = expression
41
+ .split('|')
42
+ .map((part) => part.trim());
43
+ const parts = ref.split('.').map((part) => part.trim());
44
+ let key = null;
45
+ let mode = null;
46
+ if (parts.length === 2 && parts[0] === 'value') {
47
+ key = column.name;
48
+ mode = parts[1] === 'raw' || parts[1] === 'formatted' ? parts[1] : null;
49
+ }
50
+ else if (parts.length === 4 && parts[0] === 'row') {
51
+ key = rowKeyForReference(columns, parts[1], parts[2]);
52
+ mode = parts[3] === 'raw' || parts[3] === 'formatted' ? parts[3] : null;
53
+ }
54
+ if (key === null || mode === null)
55
+ return null;
56
+ const raw = row[key];
57
+ if (raw === null || raw === undefined)
58
+ return null;
59
+ const value = mode === 'formatted' && format ? format(row, key) : String(raw);
60
+ return applyFilters(value, filters);
61
+ };
62
+ /**
63
+ * Build the links a cell offers from its column's `urls` patterns. Patterns
64
+ * whose references cannot be resolved against this row (missing field, null
65
+ * value, unknown filter) are skipped, matching dashboard behavior.
66
+ */
67
+ export const resolveColumnUrls = (args) => {
68
+ const urls = args.column.urls ?? [];
69
+ if (urls.length === 0)
70
+ return [];
71
+ if (args.row[args.column.name] === null)
72
+ return [];
73
+ const resolved = [];
74
+ for (const { label, url } of urls) {
75
+ let failed = false;
76
+ const href = url.replace(TEMPLATE_RE, (match, expression) => {
77
+ const substituted = resolveExpression(expression, args);
78
+ if (substituted === null) {
79
+ failed = true;
80
+ return match;
81
+ }
82
+ return substituted;
83
+ });
84
+ if (!failed)
85
+ resolved.push({ label, href });
86
+ }
87
+ return resolved;
88
+ };
package/dist/features.js CHANGED
@@ -211,6 +211,13 @@ export const SDK_FEATURES = [
211
211
  description: 'Reflows the layout for narrow and short tiles, such as a dashboard on a phone, so values stay readable and nothing scrolls sideways.',
212
212
  wiring: 'Rework the layout to the "Fitting the frame" section of the reusable-visualization skill. A chart type built on an older template first needs the frame runtime added to its own files, as described under viz-responsive-layout in the sdk-features skill: the VizFrame marker in src/main.jsx and the html[data-viz-frame] rules in src/index.css. Then size type, padding and gaps in rem or Tailwind classes instead of px, make rows of items wrap with a minimum item width that keeps values on one line (capped just under half the frame), remove any scroll regions of the chart\'s own so only the frame scrolls, and make a title or table header row sticky.',
213
213
  },
214
+ {
215
+ key: 'field-links',
216
+ appliesTo: ['data_app'],
217
+ label: 'Field links',
218
+ description: "Result columns carry the semantic layer's url link patterns, so values with configured links render as links, matching dashboards.",
219
+ wiring: 'In cell renderers (tables, underlying-data dialogs), call resolveColumnUrls({ column, row, columns, format }) and render the value as a link when it returns entries, styled visibly as a link.',
220
+ },
214
221
  ];
215
222
  export const SDK_FEATURE_KEYS = SDK_FEATURES.map((f) => f.key);
216
223
  /**
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "3.89.0";
1
+ export declare const SDK_VERSION = "3.91.0";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/generateSdkVersion.mjs (prebuild) — do not edit.
2
- export const SDK_VERSION = '3.89.0';
2
+ export const SDK_VERSION = '3.91.0';
package/dist/index.d.ts CHANGED
@@ -6,8 +6,9 @@ export { useLightdash } from './useLightdash';
6
6
  export { LightdashProvider, useLightdashClient } from './LightdashProvider';
7
7
  export { createApiTransport, LightdashApiError, type FetchAdapter, } from './apiTransport';
8
8
  export { createPostMessageTransport } from './postMessageTransport';
9
+ export { resolveColumnUrls, type ResolvedColumnUrl } from './columnUrls';
9
10
  export { SDK_FEATURES, SDK_FEATURE_KEYS, SDK_FIXES, SDK_FIX_KEYS, SDK_FEATURE_TARGETS, SDK_MANIFEST_MESSAGE_TYPE, type SdkFeature, type SdkFeatureTarget, type SdkFix, type SdkManifestMessage, } from './features';
10
- export type { AdditionalMetric, AiPromptRequest, AiPromptResult, Column, ColumnType, CustomDimension, DownloadResultsFileType, DownloadResultsLimit, DownloadResultsOptions, DownloadResultsResult, DownloadResultsValues, DownloadUnderlyingDataOptions, ExternalFetchMethod, ExternalFetchOptions, ExternalFetchResult, Filter, FilterOperator, FilterValue, FormatFunction, LightdashClientConfig, LightdashUser, MetricType, QueryDefinition, QueryResult, Row, Sort, TableCalculation, Transport, UnitOfTime, UnderlyingDataOptions, UnderlyingDataResult, } from './types';
11
+ export type { AdditionalMetric, AiPromptRequest, AiPromptResult, Column, ColumnType, ColumnUrl, CustomDimension, DownloadResultsFileType, DownloadResultsLimit, DownloadResultsOptions, DownloadResultsResult, DownloadResultsValues, DownloadUnderlyingDataOptions, ExternalFetchMethod, ExternalFetchOptions, ExternalFetchResult, Filter, FilterOperator, FilterValue, FormatFunction, LightdashClientConfig, LightdashUser, MetricType, QueryDefinition, QueryResult, Row, Sort, TableCalculation, Transport, UnitOfTime, UnderlyingDataOptions, UnderlyingDataResult, } from './types';
11
12
  export type { SdkExternalFetchRequest, SdkExternalFetchResponse, SdkFetchRequest, SdkFetchResponse, SdkReadyMessage, SdkScreenshotAvailableMessage, SdkScreenshotRequest, SdkScreenshotResponse, SdkGsheetExportRequest, SdkGsheetExportResponse, SdkGsheetExportColumn, SdkGsheetExportColumnType, SdkGsheetExportRow, } from './postMessageTransport';
12
13
  export type { InspectAvailableMessage, InspectSelectedMessage, } from './inspector';
13
14
  export { useDelivery } from './delivery';
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ export { LightdashProvider, useLightdashClient } from './LightdashProvider';
12
12
  // Transports
13
13
  export { createApiTransport, LightdashApiError, } from './apiTransport';
14
14
  export { createPostMessageTransport } from './postMessageTransport';
15
+ export { resolveColumnUrls } from './columnUrls';
15
16
  // Feature manifest (also exported via the "./features" subpath for hosts)
16
17
  export { SDK_FEATURES, SDK_FEATURE_KEYS, SDK_FIXES, SDK_FIX_KEYS, SDK_FEATURE_TARGETS, SDK_MANIFEST_MESSAGE_TYPE, } from './features';
17
18
  // Scheduled-delivery declarations (data apps)
package/dist/types.d.ts CHANGED
@@ -94,10 +94,29 @@ export type QueryDefinition = {
94
94
  label?: string;
95
95
  };
96
96
  export type ColumnType = 'string' | 'number' | 'date' | 'timestamp' | 'boolean';
97
+ /**
98
+ * A link pattern declared on the field in the semantic layer (`urls:` in the
99
+ * model YAML). `url` is a template: `${value.raw}` / `${value.formatted}` is
100
+ * the cell's own value, `${row.<table>.<field>.raw}` (or `.formatted`)
101
+ * references another field in the same result row, and a `| url_encode`
102
+ * filter URL-encodes the substituted value. Resolve with `resolveColumnUrls`.
103
+ */
104
+ export type ColumnUrl = {
105
+ label: string;
106
+ url: string;
107
+ };
97
108
  export type Column = {
98
109
  name: string;
99
110
  label: string;
100
111
  type: ColumnType;
112
+ /**
113
+ * The API field id (`<table>_<field>`), preserved across result paths so
114
+ * `${row.<table>.<field>}` url references resolve to the right column
115
+ * even where names are qualified differently (underlying data, joins).
116
+ */
117
+ fieldId?: string;
118
+ /** Link patterns for this field's values; render cells as links when set. */
119
+ urls?: ColumnUrl[];
101
120
  };
102
121
  export type Row = Record<string, string | number | boolean | null>;
103
122
  export type FormatFunction = (row: Row, fieldId: string) => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/query-sdk",
3
- "version": "3.89.0",
3
+ "version": "3.91.0",
4
4
  "private": false,
5
5
  "description": "SDK for building custom data apps against the Lightdash semantic layer",
6
6
  "license": "MIT",
@@ -26,7 +26,7 @@
26
26
  }
27
27
  },
28
28
  "devDependencies": {
29
- "@lightdash/common": "3.89.0",
29
+ "@lightdash/common": "3.91.0",
30
30
  "@types/node": "22.13.1",
31
31
  "@types/react": "19.3.0",
32
32
  "@types/react-dom": "19.3.0",