@fusedio/widget-sdk 0.1.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.
Files changed (43) hide show
  1. package/README.md +169 -0
  2. package/dist/bridge.d.ts +247 -0
  3. package/dist/bridge.js +61 -0
  4. package/dist/bundle.js +2 -0
  5. package/dist/define-catalog.d.ts +53 -0
  6. package/dist/define-catalog.js +3 -0
  7. package/dist/define-component.d.ts +93 -0
  8. package/dist/define-component.js +43 -0
  9. package/dist/form.d.ts +49 -0
  10. package/dist/form.js +136 -0
  11. package/dist/hooks/use-allowed-sources.d.ts +20 -0
  12. package/dist/hooks/use-allowed-sources.js +49 -0
  13. package/dist/hooks/use-allowed-udf-names.d.ts +15 -0
  14. package/dist/hooks/use-allowed-udf-names.js +42 -0
  15. package/dist/hooks/use-canvas-params.d.ts +13 -0
  16. package/dist/hooks/use-canvas-params.js +58 -0
  17. package/dist/hooks/use-duckdb-sql.d.ts +61 -0
  18. package/dist/hooks/use-duckdb-sql.js +558 -0
  19. package/dist/hooks/use-fused-param.d.ts +40 -0
  20. package/dist/hooks/use-fused-param.js +283 -0
  21. package/dist/hooks/use-json-ui-edge-animation.d.ts +22 -0
  22. package/dist/hooks/use-json-ui-edge-animation.js +26 -0
  23. package/dist/hooks/use-json-ui-log.d.ts +33 -0
  24. package/dist/hooks/use-json-ui-log.js +74 -0
  25. package/dist/hooks/use-json-ui-udf-info.d.ts +24 -0
  26. package/dist/hooks/use-json-ui-udf-info.js +23 -0
  27. package/dist/hooks/use-param-substitution.d.ts +22 -0
  28. package/dist/hooks/use-param-substitution.js +207 -0
  29. package/dist/hooks/use-udf-output.d.ts +85 -0
  30. package/dist/hooks/use-udf-output.js +202 -0
  31. package/dist/hooks/use-upload-access-check.d.ts +19 -0
  32. package/dist/hooks/use-upload-access-check.js +39 -0
  33. package/dist/hooks/use-url-signing.d.ts +42 -0
  34. package/dist/hooks/use-url-signing.js +101 -0
  35. package/dist/index.d.ts +35 -0
  36. package/dist/index.js +40 -0
  37. package/dist/protocol.d.ts +39 -0
  38. package/dist/protocol.js +32 -0
  39. package/dist/types.d.ts +84 -0
  40. package/dist/types.js +1 -0
  41. package/dist/utils/sql-placeholders.d.ts +80 -0
  42. package/dist/utils/sql-placeholders.js +204 -0
  43. package/package.json +36 -0
@@ -0,0 +1,283 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useFusedWidgetBridge, useJsonUiNode } from "../bridge";
3
+ import { useFormParams } from "../form";
4
+ import { ParameterMessageType } from "../protocol";
5
+ /**
6
+ * Two-way bind a component to a named canvas parameter.
7
+ *
8
+ * When another node broadcasts a param value (e.g. a map click, a dropdown
9
+ * selection from upstream), this hook receives it and updates `value`. When
10
+ * the user interacts locally, `setValue()` debounces and broadcasts back so
11
+ * connected UDF nodes re-run with the new value.
12
+ *
13
+ * Form integration: when this component is rendered inside a built-in Form,
14
+ * the live form-field value shadows the canvas value so siblings react
15
+ * before the form is submitted (form values are never broadcast to the
16
+ * canvas until submit).
17
+ *
18
+ * Works as plain local state if `param` is undefined or empty.
19
+ *
20
+ * Requires: a `FusedWidgetBridgeContext.Provider` ancestor. The workbench's
21
+ * `JsonUiProvider` provides one; tests use `createTestBridge()`.
22
+ *
23
+ * @example — number counter
24
+ * const { value, setValue } = useFusedParam({ param: "count", defaultValue: 0 });
25
+ * return <button onClick={() => setValue(value + 1)}>{value}</button>;
26
+ *
27
+ * @example — typed array param
28
+ * const { value, setValue } = useFusedParam({
29
+ * param: "bounds",
30
+ * defaultValue: [-74, 40, -73, 41] as [number, number, number, number],
31
+ * validate: (v): v is [number, number, number, number] =>
32
+ * Array.isArray(v) && v.length === 4 && v.every(n => typeof n === "number"),
33
+ * });
34
+ *
35
+ * @example — broadcast immediately on mouseup (bypass debounce)
36
+ * const { setValue, broadcastNow } = useFusedParam({ param: "hue", defaultValue: 0 });
37
+ * <input type="range" onChange={e => setValue(+e.target.value)} onMouseUp={broadcastNow} />
38
+ *
39
+ * @example — clear button
40
+ * const { clearValue } = useFusedParam({ param: "selection", defaultValue: null });
41
+ * <button onClick={() => clearValue(null)}>Reset</button>
42
+ */
43
+ export function useFusedParam({ param, debounceMs = 300, readOnly = false, defaultValue, broadcastDefaultValue = true, validate, preprocess, }) {
44
+ const bridge = useFusedWidgetBridge();
45
+ const { configHash } = useJsonUiNode();
46
+ const enabled = !!param;
47
+ // Stable log helper — entries appear in the workbench's runtime logs panel.
48
+ // configHash is passed through so nested JsonUiConfigHashOverride subtrees
49
+ // tag entries correctly without requiring a bridge rebuild.
50
+ const logPreview = (action, raw) => {
51
+ if (!param)
52
+ return;
53
+ if (action === "Cleared") {
54
+ bridge.log.log(`Cleared param "${param}"`, "info", configHash);
55
+ return;
56
+ }
57
+ const preview = JSON.stringify(raw);
58
+ const trimmed = preview && preview.length > 100 ? preview.slice(0, 100) + "…" : preview;
59
+ bridge.log.log(`${action} param "${param}" = ${trimmed}`, "info", configHash);
60
+ };
61
+ // Stable validator and preprocessor. We deliberately omit `defaultValue`
62
+ // from the deps so a parent re-render passing a new defaultValue literal
63
+ // doesn't churn these. The type semantics only depend on the original.
64
+ const preprocessor = useMemo(() => preprocess ?? createDefaultPreprocessor(defaultValue),
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ [preprocess]);
67
+ const validator = useMemo(() => validate ?? createDefaultValidator(defaultValue),
68
+ // eslint-disable-next-line react-hooks/exhaustive-deps
69
+ [validate]);
70
+ // Form-scoped value shadows canvas value when inside a form.
71
+ const paramNamesForForm = useMemo(() => (param ? [param] : []), [param]);
72
+ const { inForm, values: formParams } = useFormParams(paramNamesForForm);
73
+ const formValue = param ? formParams[param] : undefined;
74
+ // ── Initial value resolution ────────────────────────────────────────────
75
+ // Read snapshot once to seed local state — subsequent updates come via the
76
+ // sync effect below. Reading the bridge here is safe (synchronous).
77
+ const initialCanvasValue = useMemo(() => {
78
+ if (!enabled || !param)
79
+ return undefined;
80
+ return bridge.params.getSnapshot(param);
81
+ }, [bridge, enabled, param]);
82
+ const computeInitialValue = () => {
83
+ const raw = inForm && formValue !== undefined ? formValue : initialCanvasValue;
84
+ if (raw === undefined || raw === null)
85
+ return defaultValue;
86
+ const processed = preprocessor(raw);
87
+ return validator(processed) ? processed : defaultValue;
88
+ };
89
+ const [value, setValueState] = useState(computeInitialValue);
90
+ const valueRef = useRef(value);
91
+ valueRef.current = value;
92
+ const debounceRef = useRef(null);
93
+ const isLocalChangeRef = useRef(false);
94
+ const didBroadcastOnMountRef = useRef(false);
95
+ // Stable closure over the latest binding for the unmount cleanup.
96
+ // bridge is captured by ref too so the cleanup effect's deps can stay `[]`
97
+ // — otherwise any bridge identity flip would re-run the effect and
98
+ // broadcast a stray CLEAR for every bound input, cascading through the
99
+ // canvas-param listener fan-out.
100
+ const latestBindingRef = useRef({ enabled, param });
101
+ latestBindingRef.current = { enabled, param };
102
+ const bridgeRef = useRef(bridge);
103
+ bridgeRef.current = bridge;
104
+ // ── Clear bound param when name changes (carry-over avoidance) ──────────
105
+ const prevParamRef = useRef(param);
106
+ useEffect(() => {
107
+ const prev = prevParamRef.current;
108
+ prevParamRef.current = param;
109
+ if (prev && prev !== param) {
110
+ bridge.params.clear(prev);
111
+ }
112
+ }, [bridge, param]);
113
+ // ── Subscribe to canvas (and form) updates ──────────────────────────────
114
+ useEffect(() => {
115
+ if (!enabled || !param)
116
+ return;
117
+ if (isLocalChangeRef.current)
118
+ return;
119
+ const raw = inForm && formValue !== undefined
120
+ ? formValue
121
+ : bridge.params.getSnapshot(param);
122
+ // Do not reset to defaultValue on transient null/undefined — would wipe
123
+ // the user's local selection during share-mode filtering or fast clicks.
124
+ if (raw === undefined || raw === null)
125
+ return;
126
+ const processed = preprocessor(raw);
127
+ if (processed !== valueRef.current && validator(processed)) {
128
+ setValueState(processed);
129
+ logPreview("Received", raw);
130
+ }
131
+ // eslint-disable-next-line react-hooks/exhaustive-deps
132
+ }, [bridge, param, enabled, inForm, formValue, preprocessor, validator]);
133
+ // Subscribe to bridge canvas updates so changes from other nodes flow in.
134
+ useEffect(() => {
135
+ if (!enabled || !param)
136
+ return;
137
+ const unsub = bridge.params.subscribe(param, () => {
138
+ if (isLocalChangeRef.current)
139
+ return;
140
+ const raw = bridge.params.getSnapshot(param);
141
+ if (raw === undefined || raw === null)
142
+ return;
143
+ const processed = preprocessor(raw);
144
+ if (processed !== valueRef.current && validator(processed)) {
145
+ setValueState(processed);
146
+ logPreview("Received", raw);
147
+ }
148
+ });
149
+ return unsub;
150
+ // eslint-disable-next-line react-hooks/exhaustive-deps
151
+ }, [bridge, param, enabled, preprocessor, validator]);
152
+ // ── Broadcast plumbing ──────────────────────────────────────────────────
153
+ const broadcast = useCallback((newValue) => {
154
+ if (!enabled || !param)
155
+ return;
156
+ bridge.params.set(param, newValue, ParameterMessageType.PARAM);
157
+ bridge.edges.stopLoading();
158
+ logPreview("Broadcast", newValue);
159
+ },
160
+ // eslint-disable-next-line react-hooks/exhaustive-deps
161
+ [bridge, enabled, param]);
162
+ const broadcastNow = useCallback(() => {
163
+ if (!enabled || readOnly)
164
+ return;
165
+ if (debounceRef.current) {
166
+ clearTimeout(debounceRef.current);
167
+ debounceRef.current = null;
168
+ }
169
+ bridge.edges.startLoading();
170
+ broadcast(valueRef.current);
171
+ isLocalChangeRef.current = false;
172
+ }, [bridge, broadcast, enabled, readOnly]);
173
+ const clearValue = useCallback((newValue) => {
174
+ if (debounceRef.current) {
175
+ clearTimeout(debounceRef.current);
176
+ debounceRef.current = null;
177
+ }
178
+ setValueState(newValue);
179
+ valueRef.current = newValue;
180
+ isLocalChangeRef.current = false;
181
+ if (!enabled || !param || readOnly)
182
+ return;
183
+ bridge.params.clear(param);
184
+ bridge.edges.stopLoading();
185
+ logPreview("Cleared", null);
186
+ },
187
+ // eslint-disable-next-line react-hooks/exhaustive-deps
188
+ [bridge, enabled, param, readOnly]);
189
+ // Reset one-time mount broadcast guard when binding changes.
190
+ useEffect(() => {
191
+ didBroadcastOnMountRef.current = false;
192
+ }, [param, enabled]);
193
+ // Broadcast default value on mount when no canvas value exists.
194
+ useEffect(() => {
195
+ if (!enabled || !param || readOnly)
196
+ return;
197
+ if (!broadcastDefaultValue)
198
+ return;
199
+ if (didBroadcastOnMountRef.current)
200
+ return;
201
+ const canvasValue = bridge.params.getSnapshot(param);
202
+ if (canvasValue !== undefined && canvasValue !== null) {
203
+ didBroadcastOnMountRef.current = true;
204
+ return;
205
+ }
206
+ // Empty-string defaults are noisy — skip.
207
+ if (valueRef.current === "") {
208
+ didBroadcastOnMountRef.current = true;
209
+ return;
210
+ }
211
+ broadcast(valueRef.current);
212
+ didBroadcastOnMountRef.current = true;
213
+ }, [bridge, enabled, param, readOnly, broadcastDefaultValue, broadcast]);
214
+ const setValue = useCallback((newValue) => {
215
+ setValueState(newValue);
216
+ if (!enabled || readOnly)
217
+ return;
218
+ isLocalChangeRef.current = true;
219
+ bridge.edges.startLoading();
220
+ if (debounceRef.current)
221
+ clearTimeout(debounceRef.current);
222
+ debounceRef.current = setTimeout(() => {
223
+ broadcast(newValue);
224
+ isLocalChangeRef.current = false;
225
+ }, debounceMs);
226
+ }, [bridge, broadcast, debounceMs, enabled, readOnly]);
227
+ // Cleanup: cancel pending debounce, and clear the bound param so preset
228
+ // type-swaps don't leave stale canvas state behind. Deps are `[]` so this
229
+ // runs on unmount only — never on bridge identity flips. bridge is read
230
+ // from the ref at cleanup time.
231
+ useEffect(() => {
232
+ return () => {
233
+ if (debounceRef.current)
234
+ clearTimeout(debounceRef.current);
235
+ const { enabled, param } = latestBindingRef.current;
236
+ if (!enabled || !param)
237
+ return;
238
+ bridgeRef.current.params.clear(param);
239
+ };
240
+ // eslint-disable-next-line react-hooks/exhaustive-deps
241
+ }, []);
242
+ return { value, setValue, broadcastNow, clearValue };
243
+ }
244
+ // ============================================================================
245
+ // Default validator + preprocessor
246
+ // ============================================================================
247
+ function createDefaultValidator(defaultValue) {
248
+ if (typeof defaultValue === "string") {
249
+ return ((v) => typeof v === "string");
250
+ }
251
+ if (typeof defaultValue === "number") {
252
+ return ((v) => typeof v === "number");
253
+ }
254
+ if (typeof defaultValue === "boolean") {
255
+ return ((v) => typeof v === "boolean");
256
+ }
257
+ if (Array.isArray(defaultValue)) {
258
+ return ((v) => Array.isArray(v));
259
+ }
260
+ if (defaultValue !== null && typeof defaultValue === "object") {
261
+ return ((v) => v !== null && typeof v === "object" && !Array.isArray(v));
262
+ }
263
+ return ((_v) => true);
264
+ }
265
+ /**
266
+ * Coerces raw canvas values to T before validation. For string T this
267
+ * prevents the strict type guard from silently dropping numbers/booleans/
268
+ * arrays/objects that arrive as the param value. For other types, passthrough.
269
+ */
270
+ function createDefaultPreprocessor(defaultValue) {
271
+ if (typeof defaultValue === "string") {
272
+ return (v) => {
273
+ if (typeof v === "string")
274
+ return v;
275
+ if (Array.isArray(v))
276
+ return v.join(",");
277
+ if (v !== null && typeof v === "object")
278
+ return JSON.stringify(v);
279
+ return String(v);
280
+ };
281
+ }
282
+ return (v) => v;
283
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Controls the edge-animation pellet for the current canvas node.
3
+ *
4
+ * Call `startLoading()` when beginning async work and `stopLoading()` on
5
+ * completion — the `true → false` transition is what fires the visual
6
+ * pellet on connected outgoing canvas edges.
7
+ *
8
+ * `useFusedParam` calls these automatically around each broadcast, so use
9
+ * this hook only when you need to animate edges for non-param async work
10
+ * (e.g. a `fetch()` your component runs directly).
11
+ *
12
+ * @example
13
+ * const { startLoading, stopLoading } = useJsonUiEdgeAnimation();
14
+ * async function fetchData() {
15
+ * startLoading();
16
+ * try { await heavyCompute(); } finally { stopLoading(); }
17
+ * }
18
+ */
19
+ export declare function useJsonUiEdgeAnimation(): {
20
+ startLoading: () => void;
21
+ stopLoading: () => void;
22
+ };
@@ -0,0 +1,26 @@
1
+ import { useFusedWidgetBridge } from "../bridge";
2
+ /**
3
+ * Controls the edge-animation pellet for the current canvas node.
4
+ *
5
+ * Call `startLoading()` when beginning async work and `stopLoading()` on
6
+ * completion — the `true → false` transition is what fires the visual
7
+ * pellet on connected outgoing canvas edges.
8
+ *
9
+ * `useFusedParam` calls these automatically around each broadcast, so use
10
+ * this hook only when you need to animate edges for non-param async work
11
+ * (e.g. a `fetch()` your component runs directly).
12
+ *
13
+ * @example
14
+ * const { startLoading, stopLoading } = useJsonUiEdgeAnimation();
15
+ * async function fetchData() {
16
+ * startLoading();
17
+ * try { await heavyCompute(); } finally { stopLoading(); }
18
+ * }
19
+ */
20
+ export function useJsonUiEdgeAnimation() {
21
+ const bridge = useFusedWidgetBridge();
22
+ return {
23
+ startLoading: bridge.edges.startLoading,
24
+ stopLoading: bridge.edges.stopLoading,
25
+ };
26
+ }
@@ -0,0 +1,33 @@
1
+ import { type JsonUiLogLevel, type LogEntry } from "../bridge";
2
+ /**
3
+ * Logging hook for json-ui components. Entries appear in the workbench's
4
+ * runtime logs panel for the current node.
5
+ *
6
+ * Entries are automatically tagged with the current widget's `configHash`
7
+ * (resolved through `JsonUiNodeOverrideContext` so nested
8
+ * `JsonUiConfigHashOverride` subtrees emit correctly-scoped entries),
9
+ * which lets the workbench discard stale entries when a widget's JSON is
10
+ * edited.
11
+ *
12
+ * @example
13
+ * const { log } = useJsonUiLog();
14
+ * log("Selected city: " + city);
15
+ * log("Failed to load chart data", "error");
16
+ */
17
+ export declare function useJsonUiLog(): {
18
+ log: (message: string, level?: JsonUiLogLevel) => void;
19
+ };
20
+ /**
21
+ * Read-only hook returning the current log entries for a given node.
22
+ * Returns an empty array if `nodeId` is undefined or the node has no logs.
23
+ *
24
+ * Primarily used by the workbench's runtime logs panel — catalog components
25
+ * rarely need to read logs they wrote, but the hook is exposed for
26
+ * completeness (e.g. building a debug view component).
27
+ */
28
+ export declare function useJsonUiLogs(nodeId: string | undefined): readonly LogEntry[];
29
+ /**
30
+ * Returns a stable callback that clears all log entries for the given node.
31
+ * Returns a no-op function when `nodeId` is undefined.
32
+ */
33
+ export declare function useJsonUiLogClear(nodeId: string | undefined): () => void;
@@ -0,0 +1,74 @@
1
+ import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
2
+ import { useFusedWidgetBridge, useJsonUiNode, } from "../bridge";
3
+ const EMPTY_LOGS = Object.freeze([]);
4
+ /**
5
+ * Logging hook for json-ui components. Entries appear in the workbench's
6
+ * runtime logs panel for the current node.
7
+ *
8
+ * Entries are automatically tagged with the current widget's `configHash`
9
+ * (resolved through `JsonUiNodeOverrideContext` so nested
10
+ * `JsonUiConfigHashOverride` subtrees emit correctly-scoped entries),
11
+ * which lets the workbench discard stale entries when a widget's JSON is
12
+ * edited.
13
+ *
14
+ * @example
15
+ * const { log } = useJsonUiLog();
16
+ * log("Selected city: " + city);
17
+ * log("Failed to load chart data", "error");
18
+ */
19
+ export function useJsonUiLog() {
20
+ const bridge = useFusedWidgetBridge();
21
+ const { configHash } = useJsonUiNode();
22
+ // bridge and configHash are read from refs so the returned `log` callback
23
+ // identity is stable across re-renders. Downstream effects with `log` in
24
+ // their deps (e.g. use-duckdb-sql.ts's preprocessing effect) would
25
+ // otherwise re-fire on every bridge flip / configHash change and cascade
26
+ // setState across every SQL widget in the dashboard.
27
+ const bridgeRef = useRef(bridge);
28
+ bridgeRef.current = bridge;
29
+ const configHashRef = useRef(configHash);
30
+ configHashRef.current = configHash;
31
+ const log = useCallback((message, level = "info") => {
32
+ bridgeRef.current.log.log(message, level, configHashRef.current);
33
+ }, []);
34
+ return useMemo(() => ({ log }), [log]);
35
+ }
36
+ /**
37
+ * Read-only hook returning the current log entries for a given node.
38
+ * Returns an empty array if `nodeId` is undefined or the node has no logs.
39
+ *
40
+ * Primarily used by the workbench's runtime logs panel — catalog components
41
+ * rarely need to read logs they wrote, but the hook is exposed for
42
+ * completeness (e.g. building a debug view component).
43
+ */
44
+ export function useJsonUiLogs(nodeId) {
45
+ const bridge = useFusedWidgetBridge();
46
+ const subscribe = useCallback((cb) => {
47
+ if (!nodeId)
48
+ return () => { };
49
+ return bridge.log.subscribeLogs(nodeId, cb);
50
+ }, [bridge, nodeId]);
51
+ const snapshotRef = useRef(EMPTY_LOGS);
52
+ const getSnapshot = useCallback(() => {
53
+ if (!nodeId)
54
+ return EMPTY_LOGS;
55
+ const next = bridge.log.getLogsSnapshot(nodeId);
56
+ if (next === snapshotRef.current)
57
+ return snapshotRef.current;
58
+ snapshotRef.current = next;
59
+ return next;
60
+ }, [bridge, nodeId]);
61
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
62
+ }
63
+ /**
64
+ * Returns a stable callback that clears all log entries for the given node.
65
+ * Returns a no-op function when `nodeId` is undefined.
66
+ */
67
+ export function useJsonUiLogClear(nodeId) {
68
+ const bridge = useFusedWidgetBridge();
69
+ return useCallback(() => {
70
+ if (!nodeId)
71
+ return;
72
+ bridge.log.clearLogs(nodeId);
73
+ }, [bridge, nodeId]);
74
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Returns the identity of the current canvas node.
3
+ *
4
+ * `udfUniqueId` and `udfName` are used internally by `useFusedParam` for
5
+ * edge-based param routing. Catalog components rarely need to read these
6
+ * directly — they're surfaced primarily so logs and debugging can be
7
+ * scoped to a node.
8
+ *
9
+ * Reads from `JsonUiNodeOverrideContext` first (so nested
10
+ * `JsonUiConfigHashOverride` subtrees see their override), falling back to
11
+ * the bridge's node identity.
12
+ *
13
+ * Returns `{ udfUniqueId: undefined, udfName: undefined, configHash: undefined }`
14
+ * when the surrounding bridge has not populated them.
15
+ *
16
+ * @example
17
+ * const { udfName } = useJsonUiUdfInfo();
18
+ * console.log("I am node:", udfName);
19
+ */
20
+ export declare function useJsonUiUdfInfo(): {
21
+ udfUniqueId: string | undefined;
22
+ udfName: string | undefined;
23
+ configHash: string | undefined;
24
+ };
@@ -0,0 +1,23 @@
1
+ import { useJsonUiNode } from "../bridge";
2
+ /**
3
+ * Returns the identity of the current canvas node.
4
+ *
5
+ * `udfUniqueId` and `udfName` are used internally by `useFusedParam` for
6
+ * edge-based param routing. Catalog components rarely need to read these
7
+ * directly — they're surfaced primarily so logs and debugging can be
8
+ * scoped to a node.
9
+ *
10
+ * Reads from `JsonUiNodeOverrideContext` first (so nested
11
+ * `JsonUiConfigHashOverride` subtrees see their override), falling back to
12
+ * the bridge's node identity.
13
+ *
14
+ * Returns `{ udfUniqueId: undefined, udfName: undefined, configHash: undefined }`
15
+ * when the surrounding bridge has not populated them.
16
+ *
17
+ * @example
18
+ * const { udfName } = useJsonUiUdfInfo();
19
+ * console.log("I am node:", udfName);
20
+ */
21
+ export function useJsonUiUdfInfo() {
22
+ return useJsonUiNode();
23
+ }
@@ -0,0 +1,22 @@
1
+ import type { ParamSubstitutionOptions, ParamSubstitutionResult } from "../types";
2
+ /**
3
+ * Reactively resolve `$param_name` and `{{udf_name}}` placeholders.
4
+ *
5
+ * - `$param_name` is substituted from canvas params (edge-filtered) and
6
+ * form-scoped values when inside a form.
7
+ * - `{{udf_name}}` (and variants like `{{udf_name.col}}`, `{{udf_name?k=v}}`,
8
+ * HTML template node refs) is resolved by the host via the template bridge.
9
+ *
10
+ * Returns `{ value, loading }`. `loading` is `true` only while `{{udf}}`
11
+ * data is being fetched; pure `$param` templates never trigger loading.
12
+ *
13
+ * @example — dynamic label
14
+ * const { value } = useParamSubstitution("Selected region: $region");
15
+ *
16
+ * @example — SQL fragment with missing-param preservation
17
+ * const { value: where } = useParamSubstitution(
18
+ * "WHERE city = '$city'",
19
+ * { preserveMissingParams: true }
20
+ * );
21
+ */
22
+ export declare function useParamSubstitution(template: string | undefined, options?: ParamSubstitutionOptions): ParamSubstitutionResult;