@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,207 @@
1
+ /**
2
+ * Reactively resolve `$param` and `{{udf}}` placeholders in a template.
3
+ *
4
+ * Pure-`$param` templates are handled in-SDK with a simple regex
5
+ * substitution. Templates that reference `{{udf}}` (potentially with
6
+ * `?overrides`, column/index access, or HTML template node recursion)
7
+ * delegate to `bridge.template.render` so the host can use its rich UDF
8
+ * machinery. The SDK orchestrates state, cancellation, and re-runs.
9
+ */
10
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore, useCallback, } from "react";
11
+ import { useFusedWidgetBridge } from "../bridge";
12
+ import { useFormParams } from "../form";
13
+ import { useCanvasParams } from "./use-canvas-params";
14
+ const PARAM_TOKEN_RE = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
15
+ const UDF_PLACEHOLDER_RE = /\{\{[\s\S]*?\}\}/;
16
+ // Mirrors `parseInlineUdfPlaceholders` for the purpose of finding which UDF
17
+ // names a template references. We only need the leading identifier — full
18
+ // access-path / override parsing is the host's concern.
19
+ const UDF_NAME_EXTRACT_RE = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\b/g;
20
+ /**
21
+ * Reactively resolve `$param_name` and `{{udf_name}}` placeholders.
22
+ *
23
+ * - `$param_name` is substituted from canvas params (edge-filtered) and
24
+ * form-scoped values when inside a form.
25
+ * - `{{udf_name}}` (and variants like `{{udf_name.col}}`, `{{udf_name?k=v}}`,
26
+ * HTML template node refs) is resolved by the host via the template bridge.
27
+ *
28
+ * Returns `{ value, loading }`. `loading` is `true` only while `{{udf}}`
29
+ * data is being fetched; pure `$param` templates never trigger loading.
30
+ *
31
+ * @example — dynamic label
32
+ * const { value } = useParamSubstitution("Selected region: $region");
33
+ *
34
+ * @example — SQL fragment with missing-param preservation
35
+ * const { value: where } = useParamSubstitution(
36
+ * "WHERE city = '$city'",
37
+ * { preserveMissingParams: true }
38
+ * );
39
+ */
40
+ export function useParamSubstitution(template, options = {}) {
41
+ const safeTemplate = template ?? "";
42
+ const preserveMissingParams = options.preserveMissingParams ?? false;
43
+ const bridge = useFusedWidgetBridge();
44
+ const paramNames = useMemo(() => extractParamNames(safeTemplate), [safeTemplate]);
45
+ const canvasValues = useCanvasParams(paramNames);
46
+ const { inForm, values: formValues } = useFormParams(paramNames);
47
+ const paramValues = useMemo(() => (inForm ? { ...canvasValues, ...formValues } : canvasValues), [canvasValues, formValues, inForm]);
48
+ const hasUdfRefs = useMemo(() => UDF_PLACEHOLDER_RE.test(safeTemplate), [safeTemplate]);
49
+ // ── Fast path: no UDF refs ─────────────────────────────────────────────
50
+ const pureParamValue = useMemo(() => {
51
+ if (hasUdfRefs)
52
+ return "";
53
+ return substituteParams(safeTemplate, paramValues, preserveMissingParams);
54
+ }, [safeTemplate, paramValues, preserveMissingParams, hasUdfRefs]);
55
+ // ── Slow path: UDF refs ────────────────────────────────────────────────
56
+ // Extract referenced UDF names so we can subscribe to their outputs.
57
+ // When any of them re-executes we bump a tick and the effect below
58
+ // re-runs `bridge.template.render` with fresh data.
59
+ const referencedUdfNames = useMemo(() => {
60
+ if (!hasUdfRefs)
61
+ return [];
62
+ const seen = new Set();
63
+ const out = [];
64
+ UDF_NAME_EXTRACT_RE.lastIndex = 0;
65
+ let m;
66
+ while ((m = UDF_NAME_EXTRACT_RE.exec(safeTemplate)) !== null) {
67
+ if (!seen.has(m[1])) {
68
+ seen.add(m[1]);
69
+ out.push(m[1]);
70
+ }
71
+ }
72
+ return out;
73
+ }, [safeTemplate, hasUdfRefs]);
74
+ // Subscribe to host re-render triggers (UDF outputs + topology) so that
75
+ // the async render re-runs when an upstream UDF re-executes or edges shift.
76
+ const templateChangeKey = useTemplateBridgeChangeKey(bridge, referencedUdfNames);
77
+ const [resolved, setResolved] = useState(() => ({ key: "", value: "" }));
78
+ const [loading, setLoading] = useState(false);
79
+ // Render key includes template + paramValues + the host change ticker.
80
+ const renderKey = useMemo(() => {
81
+ return JSON.stringify({
82
+ template: safeTemplate,
83
+ paramValues,
84
+ preserveMissingParams,
85
+ tick: templateChangeKey,
86
+ });
87
+ }, [safeTemplate, paramValues, preserveMissingParams, templateChangeKey]);
88
+ useEffect(() => {
89
+ if (!hasUdfRefs) {
90
+ setLoading(false);
91
+ return;
92
+ }
93
+ let cancelled = false;
94
+ const controller = new AbortController();
95
+ setLoading(true);
96
+ bridge.template
97
+ .render(safeTemplate, paramValues, {
98
+ preserveMissingParams,
99
+ signal: controller.signal,
100
+ })
101
+ .then((result) => {
102
+ if (cancelled)
103
+ return;
104
+ setResolved((prev) => prev.key === renderKey && prev.value === result.value
105
+ ? prev
106
+ : { key: renderKey, value: result.value });
107
+ setLoading(result.loading);
108
+ }, (err) => {
109
+ if (cancelled)
110
+ return;
111
+ if (err?.name === "AbortError")
112
+ return;
113
+ setLoading(false);
114
+ });
115
+ return () => {
116
+ cancelled = true;
117
+ controller.abort();
118
+ };
119
+ }, [
120
+ bridge,
121
+ hasUdfRefs,
122
+ safeTemplate,
123
+ paramValues,
124
+ preserveMissingParams,
125
+ renderKey,
126
+ ]);
127
+ const value = useMemo(() => {
128
+ if (!hasUdfRefs)
129
+ return pureParamValue;
130
+ if (resolved.key === renderKey)
131
+ return resolved.value;
132
+ // Loading placeholder: ask host for best-effort sync render.
133
+ try {
134
+ return bridge.template.renderLoading(safeTemplate, paramValues, {
135
+ preserveMissingParams,
136
+ });
137
+ }
138
+ catch {
139
+ return safeTemplate;
140
+ }
141
+ }, [
142
+ bridge,
143
+ hasUdfRefs,
144
+ pureParamValue,
145
+ resolved,
146
+ renderKey,
147
+ safeTemplate,
148
+ paramValues,
149
+ preserveMissingParams,
150
+ ]);
151
+ return { value, loading: hasUdfRefs ? loading : false };
152
+ }
153
+ function extractParamNames(template) {
154
+ const names = [];
155
+ const seen = new Set();
156
+ for (const match of template.matchAll(PARAM_TOKEN_RE)) {
157
+ const name = match[1];
158
+ if (!seen.has(name)) {
159
+ seen.add(name);
160
+ names.push(name);
161
+ }
162
+ }
163
+ return names;
164
+ }
165
+ function substituteParams(template, values, preserveMissingParams) {
166
+ return template.replace(PARAM_TOKEN_RE, (match, name) => {
167
+ const v = values[name];
168
+ if (v === undefined || v === null) {
169
+ return preserveMissingParams ? match : "";
170
+ }
171
+ return stringifyParamValue(v);
172
+ });
173
+ }
174
+ function stringifyParamValue(value) {
175
+ if (value === undefined || value === null)
176
+ return "";
177
+ if (typeof value === "string")
178
+ return value;
179
+ if (typeof value === "number" || typeof value === "boolean")
180
+ return String(value);
181
+ const serialized = JSON.stringify(value);
182
+ return serialized ?? "";
183
+ }
184
+ /**
185
+ * Subscribe to the template bridge's change signal + every referenced UDF's
186
+ * output via `bridge.udfs.subscribeOutput`. The returned numeric key
187
+ * changes whenever a re-render could produce a different result.
188
+ */
189
+ function useTemplateBridgeChangeKey(bridge, referencedUdfNames) {
190
+ const tickRef = useRef(0);
191
+ const namesKey = referencedUdfNames.slice().sort().join("|");
192
+ const subscribe = useCallback((cb) => {
193
+ const fire = () => {
194
+ tickRef.current += 1;
195
+ cb();
196
+ };
197
+ const unsubs = [bridge.template.subscribe(fire)];
198
+ for (const name of referencedUdfNames) {
199
+ unsubs.push(bridge.udfs.subscribeOutput(name, fire));
200
+ }
201
+ return () => unsubs.forEach((u) => u());
202
+ },
203
+ // eslint-disable-next-line react-hooks/exhaustive-deps
204
+ [bridge, namesKey]);
205
+ const getSnapshot = useCallback(() => tickRef.current, []);
206
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
207
+ }
@@ -0,0 +1,85 @@
1
+ import { type UdfOutputSnapshot } from "../bridge";
2
+ /**
3
+ * Subscribe to a UDF's output by name. Returns the current `UdfOutputSnapshot`
4
+ * (data + execution status + optional error + VFS filename) or `undefined`
5
+ * if the UDF is not present in the canvas.
6
+ *
7
+ * Re-renders when the UDF's results change (after re-execution) or when its
8
+ * execution status flips.
9
+ *
10
+ * @example
11
+ * const out = useUdfOutputByName("cities_udf");
12
+ * if (!out) return <div>UDF not found</div>;
13
+ * if (out.isExecutionInProgress) return <div>Loading…</div>;
14
+ * if (out.error) return <div>Error: {out.error}</div>;
15
+ */
16
+ export declare function useUdfOutputByName(udfName: string | undefined): UdfOutputSnapshot | undefined;
17
+ /**
18
+ * Request the workbench to re-execute a named UDF (e.g. after the user
19
+ * clicks a "Refresh" button). No-op in test harnesses without re-execution.
20
+ */
21
+ export declare function useRequestUdfReexecute(): (udfName: string) => void;
22
+ export type ParsedUdfQuery = {
23
+ udfName: string;
24
+ columnName: string;
25
+ index?: number;
26
+ } | null;
27
+ /**
28
+ * Fast check whether a string is `{{udf.col}}` or `{{udf.col[idx]}}`.
29
+ *
30
+ * @example
31
+ * isUdfQuery("{{my_udf.city}}") // true
32
+ * isUdfQuery("{{my_udf.city[0]}}") // true
33
+ * isUdfQuery("not a query") // false
34
+ */
35
+ export declare function isUdfQuery(query?: string): boolean;
36
+ /**
37
+ * Parse a UDF column query string. Returns `null` for unrecognised input.
38
+ *
39
+ * @example
40
+ * parseUdfColumnQuery("{{my_udf.city}}") // { udfName: "my_udf", columnName: "city" }
41
+ * parseUdfColumnQuery("{{my_udf.city[0]}}") // { ..., index: 0 }
42
+ */
43
+ export declare function parseUdfColumnQuery(query?: string): ParsedUdfQuery;
44
+ export interface UseUdfDataFrameSampleOptions {
45
+ udfName?: string;
46
+ sampleSize?: number;
47
+ }
48
+ export interface UseUdfDataFrameSampleResult {
49
+ loading: boolean;
50
+ errorMessage: string | null;
51
+ isError: boolean;
52
+ columns: string[];
53
+ rows: Record<string, unknown>[];
54
+ requestReexecute: () => void;
55
+ }
56
+ /**
57
+ * Pull a small sample of rows from a UDF's DataFrame output and derive
58
+ * column names. Used by dropdowns and galleries that populate options
59
+ * from UDF data.
60
+ */
61
+ export declare function useUdfDataFrameSample({ udfName, sampleSize, }: UseUdfDataFrameSampleOptions): UseUdfDataFrameSampleResult;
62
+ export interface UseUdfColumnValuesResult {
63
+ values: unknown[];
64
+ loading: boolean;
65
+ }
66
+ /**
67
+ * Read all values from a UDF column using `{{udf.col}}` query syntax.
68
+ *
69
+ * @example
70
+ * const { values, loading } = useUdfColumnValues("{{my_udf.city}}");
71
+ * // values = ["NYC", "LA", "SF", ...]
72
+ */
73
+ export declare function useUdfColumnValues(query?: string, sampleSize?: number): UseUdfColumnValuesResult;
74
+ export interface UseUdfColumnValueResult {
75
+ value: unknown | null;
76
+ loading: boolean;
77
+ }
78
+ /**
79
+ * Read a single value from a UDF column at a specific index.
80
+ *
81
+ * @example
82
+ * const { value, loading } = useUdfColumnValue("{{my_udf.city[0]}}");
83
+ * // value = "NYC"
84
+ */
85
+ export declare function useUdfColumnValue(query?: string, sampleSize?: number): UseUdfColumnValueResult;
@@ -0,0 +1,202 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore, } from "react";
2
+ import { useFusedWidgetBridge } from "../bridge";
3
+ /**
4
+ * Subscribe to a UDF's output by name. Returns the current `UdfOutputSnapshot`
5
+ * (data + execution status + optional error + VFS filename) or `undefined`
6
+ * if the UDF is not present in the canvas.
7
+ *
8
+ * Re-renders when the UDF's results change (after re-execution) or when its
9
+ * execution status flips.
10
+ *
11
+ * @example
12
+ * const out = useUdfOutputByName("cities_udf");
13
+ * if (!out) return <div>UDF not found</div>;
14
+ * if (out.isExecutionInProgress) return <div>Loading…</div>;
15
+ * if (out.error) return <div>Error: {out.error}</div>;
16
+ */
17
+ export function useUdfOutputByName(udfName) {
18
+ const bridge = useFusedWidgetBridge();
19
+ const subscribe = useCallback((cb) => {
20
+ if (!udfName)
21
+ return () => { };
22
+ return bridge.udfs.subscribeOutput(udfName, cb);
23
+ }, [bridge, udfName]);
24
+ const snapshotRef = useRef(undefined);
25
+ const getSnapshot = useCallback(() => {
26
+ if (!udfName)
27
+ return undefined;
28
+ const next = bridge.udfs.getOutputSnapshot(udfName);
29
+ const prev = snapshotRef.current;
30
+ if (snapshotsEqual(prev, next))
31
+ return prev;
32
+ snapshotRef.current = next;
33
+ return next;
34
+ }, [bridge, udfName]);
35
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
36
+ }
37
+ /**
38
+ * Request the workbench to re-execute a named UDF (e.g. after the user
39
+ * clicks a "Refresh" button). No-op in test harnesses without re-execution.
40
+ */
41
+ export function useRequestUdfReexecute() {
42
+ const bridge = useFusedWidgetBridge();
43
+ return useCallback((udfName) => bridge.udfs.requestReexecute(udfName), [bridge]);
44
+ }
45
+ function snapshotsEqual(a, b) {
46
+ if (a === b)
47
+ return true;
48
+ if (!a || !b)
49
+ return false;
50
+ return (a.data === b.data &&
51
+ a.isExecutionInProgress === b.isExecutionInProgress &&
52
+ a.error === b.error &&
53
+ a.vfsFilename === b.vfsFilename);
54
+ }
55
+ // ============================================================================
56
+ // UDF column query convenience hooks
57
+ // ============================================================================
58
+ const UDF_QUERY_REGEX = /^\{\{(\w+)\.(\w+)(?:\[(\d+)\])?\}\}$/;
59
+ /**
60
+ * Fast check whether a string is `{{udf.col}}` or `{{udf.col[idx]}}`.
61
+ *
62
+ * @example
63
+ * isUdfQuery("{{my_udf.city}}") // true
64
+ * isUdfQuery("{{my_udf.city[0]}}") // true
65
+ * isUdfQuery("not a query") // false
66
+ */
67
+ export function isUdfQuery(query) {
68
+ if (!query || typeof query !== "string")
69
+ return false;
70
+ return UDF_QUERY_REGEX.test(query);
71
+ }
72
+ /**
73
+ * Parse a UDF column query string. Returns `null` for unrecognised input.
74
+ *
75
+ * @example
76
+ * parseUdfColumnQuery("{{my_udf.city}}") // { udfName: "my_udf", columnName: "city" }
77
+ * parseUdfColumnQuery("{{my_udf.city[0]}}") // { ..., index: 0 }
78
+ */
79
+ export function parseUdfColumnQuery(query) {
80
+ if (!isUdfQuery(query))
81
+ return null;
82
+ const match = query.match(UDF_QUERY_REGEX);
83
+ if (!match)
84
+ return null;
85
+ const [, udfName, columnName, indexStr] = match;
86
+ const index = indexStr !== undefined ? parseInt(indexStr, 10) : undefined;
87
+ return { udfName, columnName, index };
88
+ }
89
+ function isDataSourceLike(value) {
90
+ return (!!value &&
91
+ typeof value === "object" &&
92
+ typeof value.getRows === "function");
93
+ }
94
+ /**
95
+ * Pull a small sample of rows from a UDF's DataFrame output and derive
96
+ * column names. Used by dropdowns and galleries that populate options
97
+ * from UDF data.
98
+ */
99
+ export function useUdfDataFrameSample({ udfName, sampleSize = 200, }) {
100
+ const snapshot = useUdfOutputByName(udfName);
101
+ const reexecute = useRequestUdfReexecute();
102
+ const [rows, setRows] = useState([]);
103
+ const [columns, setColumns] = useState([]);
104
+ useEffect(() => {
105
+ let cancelled = false;
106
+ const data = snapshot?.data;
107
+ if (!data || !isDataSourceLike(data)) {
108
+ setRows([]);
109
+ setColumns([]);
110
+ return;
111
+ }
112
+ (async () => {
113
+ try {
114
+ const rawRows = await data.getRows(0, Math.max(0, sampleSize));
115
+ if (cancelled)
116
+ return;
117
+ const normalized = rawRows.map((r) => {
118
+ const obj = r;
119
+ if (obj &&
120
+ typeof obj === "object" &&
121
+ obj.properties &&
122
+ typeof obj.properties === "object") {
123
+ return obj.properties;
124
+ }
125
+ return r;
126
+ });
127
+ setRows(normalized);
128
+ const cols = Array.from(new Set(normalized.flatMap((r) => Object.keys(r ?? {}))));
129
+ setColumns(cols);
130
+ }
131
+ catch {
132
+ if (cancelled)
133
+ return;
134
+ setRows([]);
135
+ setColumns([]);
136
+ }
137
+ })();
138
+ return () => {
139
+ cancelled = true;
140
+ };
141
+ }, [snapshot?.data, sampleSize]);
142
+ const requestReexecute = useCallback(() => {
143
+ if (udfName)
144
+ reexecute(udfName);
145
+ }, [reexecute, udfName]);
146
+ return {
147
+ loading: snapshot?.isExecutionInProgress ?? false,
148
+ errorMessage: snapshot?.error ?? null,
149
+ isError: Boolean(snapshot?.error),
150
+ columns,
151
+ rows,
152
+ requestReexecute,
153
+ };
154
+ }
155
+ /**
156
+ * Read all values from a UDF column using `{{udf.col}}` query syntax.
157
+ *
158
+ * @example
159
+ * const { values, loading } = useUdfColumnValues("{{my_udf.city}}");
160
+ * // values = ["NYC", "LA", "SF", ...]
161
+ */
162
+ export function useUdfColumnValues(query, sampleSize = 200) {
163
+ const isValid = isUdfQuery(query);
164
+ const parsed = useMemo(() => (isValid ? parseUdfColumnQuery(query) : null), [isValid, query]);
165
+ const { rows, loading } = useUdfDataFrameSample({
166
+ udfName: parsed?.udfName,
167
+ sampleSize,
168
+ });
169
+ const values = useMemo(() => {
170
+ if (!parsed || !parsed.columnName)
171
+ return [];
172
+ return rows
173
+ .map((row) => row?.[parsed.columnName])
174
+ .filter((v) => v !== undefined && v !== null);
175
+ }, [rows, parsed]);
176
+ return { values, loading: isValid ? loading : false };
177
+ }
178
+ /**
179
+ * Read a single value from a UDF column at a specific index.
180
+ *
181
+ * @example
182
+ * const { value, loading } = useUdfColumnValue("{{my_udf.city[0]}}");
183
+ * // value = "NYC"
184
+ */
185
+ export function useUdfColumnValue(query, sampleSize = 200) {
186
+ const isValid = isUdfQuery(query);
187
+ const parsed = useMemo(() => (isValid ? parseUdfColumnQuery(query) : null), [isValid, query]);
188
+ const { rows, loading } = useUdfDataFrameSample({
189
+ udfName: parsed?.udfName,
190
+ sampleSize,
191
+ });
192
+ const value = useMemo(() => {
193
+ if (!parsed || !parsed.columnName || parsed.index === undefined)
194
+ return null;
195
+ const row = rows[parsed.index];
196
+ if (!row)
197
+ return null;
198
+ const v = row[parsed.columnName];
199
+ return v !== undefined ? v : null;
200
+ }, [rows, parsed]);
201
+ return { value, loading: isValid ? loading : false };
202
+ }
@@ -0,0 +1,19 @@
1
+ export type UploadAccessState = {
2
+ status: "idle";
3
+ } | {
4
+ status: "checking";
5
+ } | {
6
+ status: "allowed";
7
+ } | {
8
+ status: "denied";
9
+ message: string;
10
+ };
11
+ /**
12
+ * Check whether the current user has write access to a destination path
13
+ * (S3, GCS, etc.). Used by the `file-upload` widget to surface a clear
14
+ * error before the user attempts to upload.
15
+ *
16
+ * Returns a state machine value `{ status: "idle" | "checking" | "allowed" |
17
+ * "denied" }` that you can branch on directly in render.
18
+ */
19
+ export declare function useUploadAccessCheck(destinationPath: string | undefined, enabled: boolean): UploadAccessState;
@@ -0,0 +1,39 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useFusedWidgetBridge } from "../bridge";
3
+ /**
4
+ * Check whether the current user has write access to a destination path
5
+ * (S3, GCS, etc.). Used by the `file-upload` widget to surface a clear
6
+ * error before the user attempts to upload.
7
+ *
8
+ * Returns a state machine value `{ status: "idle" | "checking" | "allowed" |
9
+ * "denied" }` that you can branch on directly in render.
10
+ */
11
+ export function useUploadAccessCheck(destinationPath, enabled) {
12
+ const bridge = useFusedWidgetBridge();
13
+ const [state, setState] = useState({ status: "idle" });
14
+ useEffect(() => {
15
+ if (!enabled || !destinationPath?.trim()) {
16
+ setState({ status: "idle" });
17
+ return;
18
+ }
19
+ let cancelled = false;
20
+ setState({ status: "checking" });
21
+ bridge.uploads.checkAccess(destinationPath).then((result) => {
22
+ if (cancelled)
23
+ return;
24
+ if (result.ok) {
25
+ setState({ status: "allowed" });
26
+ }
27
+ else {
28
+ setState({
29
+ status: "denied",
30
+ message: result.message ?? "Upload access denied.",
31
+ });
32
+ }
33
+ });
34
+ return () => {
35
+ cancelled = true;
36
+ };
37
+ }, [bridge, destinationPath, enabled]);
38
+ return state;
39
+ }
@@ -0,0 +1,42 @@
1
+ import { type SignUrlResult } from "../bridge";
2
+ /** URL schemes that require signing before fetch. */
3
+ export declare const SIGNED_URL_SCHEMES: readonly ["s3://", "gs://", "fd://"];
4
+ /**
5
+ * Manual URL signing — returns a stable `signUrl` callback you can call
6
+ * with any URL. Useful when your component needs to sign URLs imperatively
7
+ * (e.g. on user click). For reactive media src resolution, use `useMediaSrc`.
8
+ *
9
+ * @example
10
+ * const { signUrl } = useUrlSigning();
11
+ * const handleDownload = async () => {
12
+ * const { signed } = await signUrl("s3://my-bucket/file.csv");
13
+ * window.location.href = signed;
14
+ * };
15
+ */
16
+ export declare function useUrlSigning(): {
17
+ signUrl: (url: string) => Promise<SignUrlResult>;
18
+ };
19
+ export interface UseMediaSrcResult {
20
+ /** The resolved `src` URL (signed if needed, with `$param`s substituted). */
21
+ src: string | null;
22
+ /** True while substitution or signing is in flight. */
23
+ loading: boolean;
24
+ error: string | null;
25
+ /** Re-sign — call to refresh an expired URL. */
26
+ refreshSignedUrl: () => Promise<string | null>;
27
+ /** The substituted source URL prior to signing (useful for diagnostics). */
28
+ resolvedSrc: string;
29
+ /** True if the resolved URL requires signing. */
30
+ needsSigning: boolean;
31
+ }
32
+ /**
33
+ * Reactively resolve a media `src` URL. Substitutes `$param` tokens first,
34
+ * then signs S3/GCS/FD URLs. Other URL schemes pass through unchanged.
35
+ *
36
+ * @example
37
+ * const { src, loading, error } = useMediaSrc(props.imageUrl);
38
+ * if (loading) return <Spinner />;
39
+ * if (error) return <div>Failed: {error}</div>;
40
+ * return <img src={src ?? ""} alt="" />;
41
+ */
42
+ export declare function useMediaSrc(srcInput: string | undefined): UseMediaSrcResult;