@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.
- package/README.md +169 -0
- package/dist/bridge.d.ts +247 -0
- package/dist/bridge.js +61 -0
- package/dist/bundle.js +2 -0
- package/dist/define-catalog.d.ts +53 -0
- package/dist/define-catalog.js +3 -0
- package/dist/define-component.d.ts +93 -0
- package/dist/define-component.js +43 -0
- package/dist/form.d.ts +49 -0
- package/dist/form.js +136 -0
- package/dist/hooks/use-allowed-sources.d.ts +20 -0
- package/dist/hooks/use-allowed-sources.js +49 -0
- package/dist/hooks/use-allowed-udf-names.d.ts +15 -0
- package/dist/hooks/use-allowed-udf-names.js +42 -0
- package/dist/hooks/use-canvas-params.d.ts +13 -0
- package/dist/hooks/use-canvas-params.js +58 -0
- package/dist/hooks/use-duckdb-sql.d.ts +61 -0
- package/dist/hooks/use-duckdb-sql.js +558 -0
- package/dist/hooks/use-fused-param.d.ts +40 -0
- package/dist/hooks/use-fused-param.js +283 -0
- package/dist/hooks/use-json-ui-edge-animation.d.ts +22 -0
- package/dist/hooks/use-json-ui-edge-animation.js +26 -0
- package/dist/hooks/use-json-ui-log.d.ts +33 -0
- package/dist/hooks/use-json-ui-log.js +74 -0
- package/dist/hooks/use-json-ui-udf-info.d.ts +24 -0
- package/dist/hooks/use-json-ui-udf-info.js +23 -0
- package/dist/hooks/use-param-substitution.d.ts +22 -0
- package/dist/hooks/use-param-substitution.js +207 -0
- package/dist/hooks/use-udf-output.d.ts +85 -0
- package/dist/hooks/use-udf-output.js +202 -0
- package/dist/hooks/use-upload-access-check.d.ts +19 -0
- package/dist/hooks/use-upload-access-check.js +39 -0
- package/dist/hooks/use-url-signing.d.ts +42 -0
- package/dist/hooks/use-url-signing.js +101 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +40 -0
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.js +32 -0
- package/dist/types.d.ts +84 -0
- package/dist/types.js +1 -0
- package/dist/utils/sql-placeholders.d.ts +80 -0
- package/dist/utils/sql-placeholders.js +204 -0
- package/package.json +36 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
3
|
+
import { useParamSubstitution } from "./use-param-substitution";
|
|
4
|
+
/** URL schemes that require signing before fetch. */
|
|
5
|
+
export const SIGNED_URL_SCHEMES = ["s3://", "gs://", "fd://"];
|
|
6
|
+
function needsSigning(url) {
|
|
7
|
+
return SIGNED_URL_SCHEMES.some((scheme) => url.startsWith(scheme));
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Manual URL signing — returns a stable `signUrl` callback you can call
|
|
11
|
+
* with any URL. Useful when your component needs to sign URLs imperatively
|
|
12
|
+
* (e.g. on user click). For reactive media src resolution, use `useMediaSrc`.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { signUrl } = useUrlSigning();
|
|
16
|
+
* const handleDownload = async () => {
|
|
17
|
+
* const { signed } = await signUrl("s3://my-bucket/file.csv");
|
|
18
|
+
* window.location.href = signed;
|
|
19
|
+
* };
|
|
20
|
+
*/
|
|
21
|
+
export function useUrlSigning() {
|
|
22
|
+
const bridge = useFusedWidgetBridge();
|
|
23
|
+
const signUrl = useCallback((url) => bridge.signUrl(url), [bridge]);
|
|
24
|
+
return { signUrl };
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Reactively resolve a media `src` URL. Substitutes `$param` tokens first,
|
|
28
|
+
* then signs S3/GCS/FD URLs. Other URL schemes pass through unchanged.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* const { src, loading, error } = useMediaSrc(props.imageUrl);
|
|
32
|
+
* if (loading) return <Spinner />;
|
|
33
|
+
* if (error) return <div>Failed: {error}</div>;
|
|
34
|
+
* return <img src={src ?? ""} alt="" />;
|
|
35
|
+
*/
|
|
36
|
+
export function useMediaSrc(srcInput) {
|
|
37
|
+
const bridge = useFusedWidgetBridge();
|
|
38
|
+
const { value: resolvedSrc, loading: paramLoading } = useParamSubstitution(srcInput);
|
|
39
|
+
const [displaySrc, setDisplaySrc] = useState(null);
|
|
40
|
+
const [error, setError] = useState(null);
|
|
41
|
+
const [signing, setSigning] = useState(false);
|
|
42
|
+
const [refreshNonce, setRefreshNonce] = useState(0);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (paramLoading)
|
|
45
|
+
return;
|
|
46
|
+
if (!resolvedSrc) {
|
|
47
|
+
setDisplaySrc(null);
|
|
48
|
+
setError(null);
|
|
49
|
+
setSigning(false);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (!needsSigning(resolvedSrc)) {
|
|
53
|
+
setDisplaySrc(resolvedSrc);
|
|
54
|
+
setError(null);
|
|
55
|
+
setSigning(false);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let cancelled = false;
|
|
59
|
+
setSigning(true);
|
|
60
|
+
setError(null);
|
|
61
|
+
bridge
|
|
62
|
+
.signUrl(resolvedSrc)
|
|
63
|
+
.then(({ signed }) => {
|
|
64
|
+
if (cancelled)
|
|
65
|
+
return;
|
|
66
|
+
setDisplaySrc(signed ?? resolvedSrc);
|
|
67
|
+
})
|
|
68
|
+
.catch((e) => {
|
|
69
|
+
if (cancelled)
|
|
70
|
+
return;
|
|
71
|
+
setError(e instanceof Error ? e.message : "Failed to load media");
|
|
72
|
+
setDisplaySrc(null);
|
|
73
|
+
})
|
|
74
|
+
.finally(() => {
|
|
75
|
+
if (!cancelled)
|
|
76
|
+
setSigning(false);
|
|
77
|
+
});
|
|
78
|
+
return () => {
|
|
79
|
+
cancelled = true;
|
|
80
|
+
};
|
|
81
|
+
}, [bridge, paramLoading, resolvedSrc, refreshNonce]);
|
|
82
|
+
const refreshSignedUrl = useCallback(async () => {
|
|
83
|
+
if (!resolvedSrc || !needsSigning(resolvedSrc)) {
|
|
84
|
+
return resolvedSrc ?? null;
|
|
85
|
+
}
|
|
86
|
+
const { signed } = await bridge.signUrl(resolvedSrc);
|
|
87
|
+
const next = signed ?? resolvedSrc;
|
|
88
|
+
setDisplaySrc(next);
|
|
89
|
+
setError(null);
|
|
90
|
+
setRefreshNonce((n) => n + 1);
|
|
91
|
+
return next;
|
|
92
|
+
}, [bridge, resolvedSrc]);
|
|
93
|
+
return {
|
|
94
|
+
src: displaySrc,
|
|
95
|
+
loading: paramLoading || signing,
|
|
96
|
+
error,
|
|
97
|
+
refreshSignedUrl,
|
|
98
|
+
resolvedSrc,
|
|
99
|
+
needsSigning: Boolean(resolvedSrc && needsSigning(resolvedSrc)),
|
|
100
|
+
};
|
|
101
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fusedio/widget-sdk — public surface.
|
|
3
|
+
*
|
|
4
|
+
* This SDK has two parts:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Provider contract** — `FusedWidgetBridge` + `FusedWidgetBridgeContext`.
|
|
7
|
+
* Hosts (the Fused workbench, the catalog-template test harness, any
|
|
8
|
+
* future embedder) implement the bridge and provide it via the context.
|
|
9
|
+
*
|
|
10
|
+
* 2. **Hooks** — every API a component author needs. Hooks read the bridge
|
|
11
|
+
* from context and delegate state management to it. Authors call hooks;
|
|
12
|
+
* the bridge is invisible to them.
|
|
13
|
+
*
|
|
14
|
+
* Catalog components depend only on these hooks and types. They do not need
|
|
15
|
+
* to know about Jotai, BroadcastChannel internals, or the workbench source.
|
|
16
|
+
*/
|
|
17
|
+
export * from "./protocol";
|
|
18
|
+
export * from "./bridge";
|
|
19
|
+
export * from "./form";
|
|
20
|
+
export * from "./types";
|
|
21
|
+
export { defineComponent, type CatalogComponentDefinition, } from "./define-component";
|
|
22
|
+
export { defineCatalog, type CatalogDefinition, type CatalogDefinitionBase, type CatalogDefinitionWithSkill, } from "./define-catalog";
|
|
23
|
+
export { useFusedParam } from "./hooks/use-fused-param";
|
|
24
|
+
export { useCanvasParams } from "./hooks/use-canvas-params";
|
|
25
|
+
export { useAllowedSources } from "./hooks/use-allowed-sources";
|
|
26
|
+
export { useAllowedUdfNames } from "./hooks/use-allowed-udf-names";
|
|
27
|
+
export { useParamSubstitution } from "./hooks/use-param-substitution";
|
|
28
|
+
export { useUdfOutputByName, useRequestUdfReexecute, useUdfDataFrameSample, useUdfColumnValue, useUdfColumnValues, isUdfQuery, parseUdfColumnQuery, type ParsedUdfQuery, type UseUdfDataFrameSampleOptions, type UseUdfDataFrameSampleResult, type UseUdfColumnValueResult, type UseUdfColumnValuesResult, } from "./hooks/use-udf-output";
|
|
29
|
+
export { useDuckDbSqlQuery, useDuckDbSqlQueryPreprocessing, useVfsRegistration, type UseDuckDbSqlQueryOptions, type UseDuckDbSqlQueryResult, type UseDuckDbSqlQueryPreprocessingResult, } from "./hooks/use-duckdb-sql";
|
|
30
|
+
export { useUrlSigning, useMediaSrc, SIGNED_URL_SCHEMES, type UseMediaSrcResult, } from "./hooks/use-url-signing";
|
|
31
|
+
export { useUploadAccessCheck, type UploadAccessState, } from "./hooks/use-upload-access-check";
|
|
32
|
+
export { useJsonUiLog, useJsonUiLogs, useJsonUiLogClear, } from "./hooks/use-json-ui-log";
|
|
33
|
+
export { useJsonUiUdfInfo } from "./hooks/use-json-ui-udf-info";
|
|
34
|
+
export { useJsonUiEdgeAnimation } from "./hooks/use-json-ui-edge-animation";
|
|
35
|
+
export * from "./utils/sql-placeholders";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fusedio/widget-sdk — public surface.
|
|
3
|
+
*
|
|
4
|
+
* This SDK has two parts:
|
|
5
|
+
*
|
|
6
|
+
* 1. **Provider contract** — `FusedWidgetBridge` + `FusedWidgetBridgeContext`.
|
|
7
|
+
* Hosts (the Fused workbench, the catalog-template test harness, any
|
|
8
|
+
* future embedder) implement the bridge and provide it via the context.
|
|
9
|
+
*
|
|
10
|
+
* 2. **Hooks** — every API a component author needs. Hooks read the bridge
|
|
11
|
+
* from context and delegate state management to it. Authors call hooks;
|
|
12
|
+
* the bridge is invisible to them.
|
|
13
|
+
*
|
|
14
|
+
* Catalog components depend only on these hooks and types. They do not need
|
|
15
|
+
* to know about Jotai, BroadcastChannel internals, or the workbench source.
|
|
16
|
+
*/
|
|
17
|
+
// ── Part 1: Provider contract ────────────────────────────────────────────────
|
|
18
|
+
export * from "./protocol";
|
|
19
|
+
export * from "./bridge";
|
|
20
|
+
export * from "./form";
|
|
21
|
+
// ── Part 2: Hook types ───────────────────────────────────────────────────────
|
|
22
|
+
export * from "./types";
|
|
23
|
+
// ── Part 3: Catalog component registration ───────────────────────────────────
|
|
24
|
+
export { defineComponent, } from "./define-component";
|
|
25
|
+
export { defineCatalog, } from "./define-catalog";
|
|
26
|
+
// ── Part 2: Hooks ────────────────────────────────────────────────────────────
|
|
27
|
+
export { useFusedParam } from "./hooks/use-fused-param";
|
|
28
|
+
export { useCanvasParams } from "./hooks/use-canvas-params";
|
|
29
|
+
export { useAllowedSources } from "./hooks/use-allowed-sources";
|
|
30
|
+
export { useAllowedUdfNames } from "./hooks/use-allowed-udf-names";
|
|
31
|
+
export { useParamSubstitution } from "./hooks/use-param-substitution";
|
|
32
|
+
export { useUdfOutputByName, useRequestUdfReexecute, useUdfDataFrameSample, useUdfColumnValue, useUdfColumnValues, isUdfQuery, parseUdfColumnQuery, } from "./hooks/use-udf-output";
|
|
33
|
+
export { useDuckDbSqlQuery, useDuckDbSqlQueryPreprocessing, useVfsRegistration, } from "./hooks/use-duckdb-sql";
|
|
34
|
+
export { useUrlSigning, useMediaSrc, SIGNED_URL_SCHEMES, } from "./hooks/use-url-signing";
|
|
35
|
+
export { useUploadAccessCheck, } from "./hooks/use-upload-access-check";
|
|
36
|
+
export { useJsonUiLog, useJsonUiLogs, useJsonUiLogClear, } from "./hooks/use-json-ui-log";
|
|
37
|
+
export { useJsonUiUdfInfo } from "./hooks/use-json-ui-udf-info";
|
|
38
|
+
export { useJsonUiEdgeAnimation } from "./hooks/use-json-ui-edge-animation";
|
|
39
|
+
// ── Pure utilities (re-exported for advanced workbench paths) ────────────────
|
|
40
|
+
export * from "./utils/sql-placeholders";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Fused workbench listens on this BroadcastChannel for parameter updates
|
|
3
|
+
* from all components — both built-in and 3rd-party catalogs. The channel name
|
|
4
|
+
* is part of the public protocol; do not change it without coordinating with
|
|
5
|
+
* the workbench listener and all in-the-wild catalogs.
|
|
6
|
+
*/
|
|
7
|
+
export declare const PARAMETER_BROADCAST_CHANNEL = "parameter-updates";
|
|
8
|
+
/**
|
|
9
|
+
* Discriminator for parameter messages on the BroadcastChannel.
|
|
10
|
+
*
|
|
11
|
+
* - `PARAM` — generic parameter value update (the most common kind)
|
|
12
|
+
* - `RANGE` — range filter from a slider or histogram: `{ min, max }`
|
|
13
|
+
* - `VIEWPORT` — map viewport bounds: `{ west, south, east, north }`
|
|
14
|
+
* - `CLEAR` — clear a parameter for this source (value will be `null`)
|
|
15
|
+
*/
|
|
16
|
+
export declare enum ParameterMessageType {
|
|
17
|
+
PARAM = "param",
|
|
18
|
+
RANGE = "range",
|
|
19
|
+
VIEWPORT = "viewport",
|
|
20
|
+
CLEAR = "clear"
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Canonical message shape posted on the parameter-updates BroadcastChannel.
|
|
24
|
+
* All param messages — inbound and outbound — use this shape.
|
|
25
|
+
*
|
|
26
|
+
* The Fused workbench attaches additional source-identification fields
|
|
27
|
+
* (`sourceUdfUniqueId`, `sourceUdfName`, `sourceTabId`) for edge-based
|
|
28
|
+
* routing; catalog code does not need to construct these directly — use
|
|
29
|
+
* the `bridge.params.set()` method (or the `useFusedParam` hook).
|
|
30
|
+
*/
|
|
31
|
+
export interface StandardMessage {
|
|
32
|
+
type: ParameterMessageType;
|
|
33
|
+
/** Canvas parameter name, e.g. `"selected_city"`. */
|
|
34
|
+
parameter: string;
|
|
35
|
+
/** Value payload; `null` for `CLEAR`. */
|
|
36
|
+
values: unknown;
|
|
37
|
+
}
|
|
38
|
+
/** Type guard that verifies an unknown object is a valid StandardMessage. */
|
|
39
|
+
export declare function isStandardMessage(msg: unknown): msg is StandardMessage;
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Fused workbench listens on this BroadcastChannel for parameter updates
|
|
3
|
+
* from all components — both built-in and 3rd-party catalogs. The channel name
|
|
4
|
+
* is part of the public protocol; do not change it without coordinating with
|
|
5
|
+
* the workbench listener and all in-the-wild catalogs.
|
|
6
|
+
*/
|
|
7
|
+
export const PARAMETER_BROADCAST_CHANNEL = "parameter-updates";
|
|
8
|
+
/**
|
|
9
|
+
* Discriminator for parameter messages on the BroadcastChannel.
|
|
10
|
+
*
|
|
11
|
+
* - `PARAM` — generic parameter value update (the most common kind)
|
|
12
|
+
* - `RANGE` — range filter from a slider or histogram: `{ min, max }`
|
|
13
|
+
* - `VIEWPORT` — map viewport bounds: `{ west, south, east, north }`
|
|
14
|
+
* - `CLEAR` — clear a parameter for this source (value will be `null`)
|
|
15
|
+
*/
|
|
16
|
+
export var ParameterMessageType;
|
|
17
|
+
(function (ParameterMessageType) {
|
|
18
|
+
ParameterMessageType["PARAM"] = "param";
|
|
19
|
+
ParameterMessageType["RANGE"] = "range";
|
|
20
|
+
ParameterMessageType["VIEWPORT"] = "viewport";
|
|
21
|
+
ParameterMessageType["CLEAR"] = "clear";
|
|
22
|
+
})(ParameterMessageType || (ParameterMessageType = {}));
|
|
23
|
+
/** Type guard that verifies an unknown object is a valid StandardMessage. */
|
|
24
|
+
export function isStandardMessage(msg) {
|
|
25
|
+
if (typeof msg !== "object" || msg === null)
|
|
26
|
+
return false;
|
|
27
|
+
const m = msg;
|
|
28
|
+
return (typeof m.type === "string" &&
|
|
29
|
+
Object.values(ParameterMessageType).includes(m.type) &&
|
|
30
|
+
typeof m.parameter === "string" &&
|
|
31
|
+
"values" in m);
|
|
32
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The props every catalog component receives.
|
|
3
|
+
*
|
|
4
|
+
* The json-ui renderer calls your component as:
|
|
5
|
+
* ```tsx
|
|
6
|
+
* <MyComponent element={{ type: "my-type", props: { ...yourProps } }} />
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* Always destructure values from `element.props`; never spread `element` itself.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* interface Props { param: string; label?: string; step?: number }
|
|
13
|
+
* export function MyWidget({ element }: ComponentRenderProps<Props>) {
|
|
14
|
+
* const { param, label = "Value", step = 1 } = element.props;
|
|
15
|
+
* const { value, setValue } = useFusedParam({ param, defaultValue: 0 });
|
|
16
|
+
* return <button onClick={() => setValue(value + step)}>{label}: {value}</button>;
|
|
17
|
+
* }
|
|
18
|
+
*/
|
|
19
|
+
export interface ComponentRenderProps<P = Record<string, unknown>> {
|
|
20
|
+
element: {
|
|
21
|
+
type: string;
|
|
22
|
+
props: P;
|
|
23
|
+
key?: string;
|
|
24
|
+
children?: unknown[];
|
|
25
|
+
visible?: boolean;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Options for `useFusedParam`. The type parameter `T` is inferred from
|
|
30
|
+
* `defaultValue` — pass typed defaults like `0`, `""`, `[] as Foo[]` to
|
|
31
|
+
* narrow the return type.
|
|
32
|
+
*/
|
|
33
|
+
export interface UseFusedParamOptions<T> {
|
|
34
|
+
/**
|
|
35
|
+
* Canvas parameter name (kebab-case by convention).
|
|
36
|
+
* If undefined or empty, the hook behaves as local state only (no broadcast).
|
|
37
|
+
*/
|
|
38
|
+
param?: string;
|
|
39
|
+
/** Debounce delay in ms between `setValue()` and the broadcast. Default: 300. */
|
|
40
|
+
debounceMs?: number;
|
|
41
|
+
/** Accept incoming canvas values but never broadcast outgoing ones. */
|
|
42
|
+
readOnly?: boolean;
|
|
43
|
+
/** Starting value. Type `T` is inferred from this. */
|
|
44
|
+
defaultValue: T;
|
|
45
|
+
/**
|
|
46
|
+
* Broadcast `defaultValue` on mount if no canvas value exists. Default: true.
|
|
47
|
+
* Empty-string defaults are never broadcast (guarded internally).
|
|
48
|
+
*/
|
|
49
|
+
broadcastDefaultValue?: boolean;
|
|
50
|
+
/** Custom type guard run on incoming values after `preprocess`. */
|
|
51
|
+
validate?: (value: unknown) => value is T;
|
|
52
|
+
/**
|
|
53
|
+
* Coerce raw incoming values before validation. The default preprocessor
|
|
54
|
+
* coerces strings to string; all other types pass through unchanged.
|
|
55
|
+
*/
|
|
56
|
+
preprocess?: (value: unknown) => T;
|
|
57
|
+
}
|
|
58
|
+
/** Return value of `useFusedParam`. */
|
|
59
|
+
export interface UseFusedParamReturn<T> {
|
|
60
|
+
/** Current value — updated from canvas, form, or local `setValue()`. */
|
|
61
|
+
value: T;
|
|
62
|
+
/** Set locally and broadcast to canvas (debounced). Starts edge animation. */
|
|
63
|
+
setValue(newValue: T): void;
|
|
64
|
+
/** Broadcast current value immediately, bypassing the debounce timer. */
|
|
65
|
+
broadcastNow(): void;
|
|
66
|
+
/** Reset to `newValue` locally and send a CLEAR message to the canvas. */
|
|
67
|
+
clearValue(newValue: T): void;
|
|
68
|
+
}
|
|
69
|
+
export interface ParamSubstitutionOptions {
|
|
70
|
+
/**
|
|
71
|
+
* If `true`, unresolved `$param` tokens are left as-is instead of being
|
|
72
|
+
* replaced with empty string. Useful for SQL template fragments.
|
|
73
|
+
*/
|
|
74
|
+
preserveMissingParams?: boolean;
|
|
75
|
+
}
|
|
76
|
+
export interface ParamSubstitutionResult {
|
|
77
|
+
/** Template with all resolved tokens replaced. */
|
|
78
|
+
value: string;
|
|
79
|
+
/**
|
|
80
|
+
* True while `{{udf_name}}` data is being fetched. Always `false` for
|
|
81
|
+
* pure-`$param` templates.
|
|
82
|
+
*/
|
|
83
|
+
loading: boolean;
|
|
84
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parsing/substitution utilities for widget SQL strings.
|
|
3
|
+
*
|
|
4
|
+
* Two syntaxes are recognised:
|
|
5
|
+
* - `$param_name` — canvas/form param reference, substituted inline.
|
|
6
|
+
* - `{{udf_name}}` or `{{udf_name?k=v&k2=v2}}` — UDF Parquet placeholder,
|
|
7
|
+
* optionally with override params. Override suffixes accept `&` or `,`
|
|
8
|
+
* as separators and URL-decoded keys/values.
|
|
9
|
+
*
|
|
10
|
+
* These helpers are also used by the host (workbench bridge) for VFS
|
|
11
|
+
* registration; lifting them into the SDK keeps a single source of truth.
|
|
12
|
+
*/
|
|
13
|
+
export declare const SQL_PARAM_REGEX: RegExp;
|
|
14
|
+
export declare const SQL_SOURCE_PLACEHOLDER_REGEX: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* Matches `'scheme://...'` literals inside a SQL string. We deliberately
|
|
17
|
+
* limit to single-quoted string literals so that bare identifiers or
|
|
18
|
+
* comments containing a URL-like token aren't treated as paths to sign.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SIGNABLE_URL_LITERAL_REGEX: RegExp;
|
|
21
|
+
export declare function escapeSqlValue(value: unknown): string;
|
|
22
|
+
/**
|
|
23
|
+
* Substitute `$param_name` references directly into the SQL string. When a
|
|
24
|
+
* `$param` lives inside a single-quoted string literal, only the raw
|
|
25
|
+
* quote-escaped value is spliced in so we don't add nested quotes.
|
|
26
|
+
*/
|
|
27
|
+
export declare function substituteSqlParams(sql: string, paramValues: Record<string, unknown>): string;
|
|
28
|
+
/** Extract all SQL parameter names from `$param_name` references. */
|
|
29
|
+
export declare function extractSqlParams(sql: string): string[];
|
|
30
|
+
export interface SqlUdfPlaceholder {
|
|
31
|
+
/** Full match text including braces, e.g. `{{udf?city=NYC}}` */
|
|
32
|
+
match: string;
|
|
33
|
+
/** UDF name, e.g. `udf` */
|
|
34
|
+
name: string;
|
|
35
|
+
/**
|
|
36
|
+
* Parsed override params if the placeholder contains a `?...` suffix,
|
|
37
|
+
* otherwise `null`. Values are raw (URL-decoded) strings — they may still
|
|
38
|
+
* contain `$param` references that need canvas/form resolution.
|
|
39
|
+
*/
|
|
40
|
+
overrides: Record<string, string> | null;
|
|
41
|
+
start: number;
|
|
42
|
+
end: number;
|
|
43
|
+
}
|
|
44
|
+
export declare function parseOverridesString(rawOverrides: string | undefined): Record<string, string> | null;
|
|
45
|
+
/**
|
|
46
|
+
* Parse every `{{udf}}` or `{{udf?k=v&k2=v2}}` placeholder. Returns
|
|
47
|
+
* occurrences in source order, preserving duplicates so callers can
|
|
48
|
+
* substitute by start/end offsets.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseSqlUdfPlaceholders(sql: string): SqlUdfPlaceholder[];
|
|
51
|
+
/**
|
|
52
|
+
* Extract every signable URL appearing as a single-quoted string literal,
|
|
53
|
+
* deduped, in first-occurrence order.
|
|
54
|
+
*/
|
|
55
|
+
export declare function extractSignableUrls(sql: string): string[];
|
|
56
|
+
/**
|
|
57
|
+
* Replace every signable URL literal in `sql` with its signed counterpart
|
|
58
|
+
* from `signedMap`. Literals whose URL is missing from the map are left
|
|
59
|
+
* untouched.
|
|
60
|
+
*/
|
|
61
|
+
export declare function rewriteSignedUrls(sql: string, signedMap: Record<string, string>): string;
|
|
62
|
+
/** Return the param name if `value` is a single `$name` reference. */
|
|
63
|
+
export declare function getDollarRefName(value: string): string | null;
|
|
64
|
+
export interface ResolvedOverrideValue {
|
|
65
|
+
value: string;
|
|
66
|
+
unresolved: boolean;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a raw override value. If the entire value is a `$name` reference,
|
|
70
|
+
* substitute the param value; otherwise return verbatim. When the param is
|
|
71
|
+
* missing entirely (key not in the map), marks the result as `unresolved`
|
|
72
|
+
* so callers can keep the placeholder pending until upstream params settle.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveOverrideValue(rawValue: string, paramValues: Record<string, unknown>): ResolvedOverrideValue;
|
|
75
|
+
/**
|
|
76
|
+
* Canonical registry key for `(name, overrides)`. Bare placeholders use the
|
|
77
|
+
* name alone; overrides are sorted by key and joined with `&`.
|
|
78
|
+
*/
|
|
79
|
+
export declare function canonicalOverrideKey(overrides: Record<string, string>): string;
|
|
80
|
+
export declare function computePlaceholderKey(name: string, overrides: Record<string, string> | null): string;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parsing/substitution utilities for widget SQL strings.
|
|
3
|
+
*
|
|
4
|
+
* Two syntaxes are recognised:
|
|
5
|
+
* - `$param_name` — canvas/form param reference, substituted inline.
|
|
6
|
+
* - `{{udf_name}}` or `{{udf_name?k=v&k2=v2}}` — UDF Parquet placeholder,
|
|
7
|
+
* optionally with override params. Override suffixes accept `&` or `,`
|
|
8
|
+
* as separators and URL-decoded keys/values.
|
|
9
|
+
*
|
|
10
|
+
* These helpers are also used by the host (workbench bridge) for VFS
|
|
11
|
+
* registration; lifting them into the SDK keeps a single source of truth.
|
|
12
|
+
*/
|
|
13
|
+
export const SQL_PARAM_REGEX = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
14
|
+
export const SQL_SOURCE_PLACEHOLDER_REGEX = /\{\{(\w+)(?:\?([^}]*))?\}\}/g;
|
|
15
|
+
/**
|
|
16
|
+
* Matches `'scheme://...'` literals inside a SQL string. We deliberately
|
|
17
|
+
* limit to single-quoted string literals so that bare identifiers or
|
|
18
|
+
* comments containing a URL-like token aren't treated as paths to sign.
|
|
19
|
+
*/
|
|
20
|
+
export const SIGNABLE_URL_LITERAL_REGEX = /'((?:s3|gs|fd):\/\/[^'\n]+)'/g;
|
|
21
|
+
export function escapeSqlValue(value) {
|
|
22
|
+
if (value == null)
|
|
23
|
+
return "''";
|
|
24
|
+
if (typeof value === "number" && !Number.isNaN(value))
|
|
25
|
+
return String(value);
|
|
26
|
+
if (typeof value === "boolean")
|
|
27
|
+
return value ? "TRUE" : "FALSE";
|
|
28
|
+
const str = String(value);
|
|
29
|
+
return `'${str.replace(/'/g, "''")}'`;
|
|
30
|
+
}
|
|
31
|
+
function isInsideSingleQuotedSqlString(sql, offset) {
|
|
32
|
+
let inString = false;
|
|
33
|
+
for (let i = 0; i < offset; i++) {
|
|
34
|
+
if (sql[i] !== "'")
|
|
35
|
+
continue;
|
|
36
|
+
if (inString && sql[i + 1] === "'") {
|
|
37
|
+
i++;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
inString = !inString;
|
|
41
|
+
}
|
|
42
|
+
return inString;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Substitute `$param_name` references directly into the SQL string. When a
|
|
46
|
+
* `$param` lives inside a single-quoted string literal, only the raw
|
|
47
|
+
* quote-escaped value is spliced in so we don't add nested quotes.
|
|
48
|
+
*/
|
|
49
|
+
export function substituteSqlParams(sql, paramValues) {
|
|
50
|
+
return sql.replace(SQL_PARAM_REGEX, (_match, paramName, offset) => {
|
|
51
|
+
const value = paramValues[paramName];
|
|
52
|
+
const raw = value == null ? "" : String(value);
|
|
53
|
+
if (isInsideSingleQuotedSqlString(sql, offset)) {
|
|
54
|
+
return raw.replace(/'/g, "''");
|
|
55
|
+
}
|
|
56
|
+
return escapeSqlValue(value);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** Extract all SQL parameter names from `$param_name` references. */
|
|
60
|
+
export function extractSqlParams(sql) {
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const out = [];
|
|
63
|
+
SQL_PARAM_REGEX.lastIndex = 0;
|
|
64
|
+
for (const m of sql.matchAll(SQL_PARAM_REGEX)) {
|
|
65
|
+
const name = m[1];
|
|
66
|
+
if (seen.has(name))
|
|
67
|
+
continue;
|
|
68
|
+
seen.add(name);
|
|
69
|
+
out.push(name);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
export function parseOverridesString(rawOverrides) {
|
|
74
|
+
if (!rawOverrides)
|
|
75
|
+
return null;
|
|
76
|
+
const result = {};
|
|
77
|
+
let parsedAny = false;
|
|
78
|
+
for (const pair of rawOverrides.split(/[&,]/)) {
|
|
79
|
+
if (!pair)
|
|
80
|
+
continue;
|
|
81
|
+
const eq = pair.indexOf("=");
|
|
82
|
+
if (eq === -1)
|
|
83
|
+
continue;
|
|
84
|
+
const rawKey = pair.slice(0, eq);
|
|
85
|
+
const rawValue = pair.slice(eq + 1);
|
|
86
|
+
let key;
|
|
87
|
+
let value;
|
|
88
|
+
try {
|
|
89
|
+
key = decodeURIComponent(rawKey);
|
|
90
|
+
value = decodeURIComponent(rawValue);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
key = rawKey;
|
|
94
|
+
value = rawValue;
|
|
95
|
+
}
|
|
96
|
+
if (!key)
|
|
97
|
+
continue;
|
|
98
|
+
result[key] = value;
|
|
99
|
+
parsedAny = true;
|
|
100
|
+
}
|
|
101
|
+
return parsedAny ? result : null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Parse every `{{udf}}` or `{{udf?k=v&k2=v2}}` placeholder. Returns
|
|
105
|
+
* occurrences in source order, preserving duplicates so callers can
|
|
106
|
+
* substitute by start/end offsets.
|
|
107
|
+
*/
|
|
108
|
+
export function parseSqlUdfPlaceholders(sql) {
|
|
109
|
+
const placeholders = [];
|
|
110
|
+
let match;
|
|
111
|
+
SQL_SOURCE_PLACEHOLDER_REGEX.lastIndex = 0;
|
|
112
|
+
while ((match = SQL_SOURCE_PLACEHOLDER_REGEX.exec(sql)) !== null) {
|
|
113
|
+
const [fullMatch, name, rawOverrides] = match;
|
|
114
|
+
placeholders.push({
|
|
115
|
+
match: fullMatch,
|
|
116
|
+
name,
|
|
117
|
+
overrides: parseOverridesString(rawOverrides),
|
|
118
|
+
start: match.index,
|
|
119
|
+
end: match.index + fullMatch.length,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return placeholders;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Extract every signable URL appearing as a single-quoted string literal,
|
|
126
|
+
* deduped, in first-occurrence order.
|
|
127
|
+
*/
|
|
128
|
+
export function extractSignableUrls(sql) {
|
|
129
|
+
if (!sql)
|
|
130
|
+
return [];
|
|
131
|
+
const seen = new Set();
|
|
132
|
+
const out = [];
|
|
133
|
+
SIGNABLE_URL_LITERAL_REGEX.lastIndex = 0;
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = SIGNABLE_URL_LITERAL_REGEX.exec(sql)) !== null) {
|
|
136
|
+
const url = match[1];
|
|
137
|
+
if (!seen.has(url)) {
|
|
138
|
+
seen.add(url);
|
|
139
|
+
out.push(url);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Replace every signable URL literal in `sql` with its signed counterpart
|
|
146
|
+
* from `signedMap`. Literals whose URL is missing from the map are left
|
|
147
|
+
* untouched.
|
|
148
|
+
*/
|
|
149
|
+
export function rewriteSignedUrls(sql, signedMap) {
|
|
150
|
+
if (!sql)
|
|
151
|
+
return sql;
|
|
152
|
+
return sql.replace(SIGNABLE_URL_LITERAL_REGEX, (literal, url) => {
|
|
153
|
+
const signed = signedMap[url];
|
|
154
|
+
if (!signed)
|
|
155
|
+
return literal;
|
|
156
|
+
return `'${signed}'`;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
// ============================================================================
|
|
160
|
+
// Override-param resolution
|
|
161
|
+
// ============================================================================
|
|
162
|
+
const DOLLAR_REF_RE = /^\$([a-zA-Z_][a-zA-Z0-9_]*)$/;
|
|
163
|
+
/** Return the param name if `value` is a single `$name` reference. */
|
|
164
|
+
export function getDollarRefName(value) {
|
|
165
|
+
const trimmed = value.trim();
|
|
166
|
+
const m = DOLLAR_REF_RE.exec(trimmed);
|
|
167
|
+
return m ? m[1] : null;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Resolve a raw override value. If the entire value is a `$name` reference,
|
|
171
|
+
* substitute the param value; otherwise return verbatim. When the param is
|
|
172
|
+
* missing entirely (key not in the map), marks the result as `unresolved`
|
|
173
|
+
* so callers can keep the placeholder pending until upstream params settle.
|
|
174
|
+
*/
|
|
175
|
+
export function resolveOverrideValue(rawValue, paramValues) {
|
|
176
|
+
const m = DOLLAR_REF_RE.exec(rawValue);
|
|
177
|
+
if (!m) {
|
|
178
|
+
return { value: rawValue, unresolved: false };
|
|
179
|
+
}
|
|
180
|
+
const name = m[1];
|
|
181
|
+
if (!(name in paramValues)) {
|
|
182
|
+
return { value: rawValue, unresolved: true };
|
|
183
|
+
}
|
|
184
|
+
const resolved = paramValues[name];
|
|
185
|
+
if (resolved == null) {
|
|
186
|
+
return { value: "", unresolved: false };
|
|
187
|
+
}
|
|
188
|
+
return { value: String(resolved), unresolved: false };
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Canonical registry key for `(name, overrides)`. Bare placeholders use the
|
|
192
|
+
* name alone; overrides are sorted by key and joined with `&`.
|
|
193
|
+
*/
|
|
194
|
+
export function canonicalOverrideKey(overrides) {
|
|
195
|
+
return Object.keys(overrides)
|
|
196
|
+
.sort()
|
|
197
|
+
.map((k) => `${k}=${overrides[k]}`)
|
|
198
|
+
.join("&");
|
|
199
|
+
}
|
|
200
|
+
export function computePlaceholderKey(name, overrides) {
|
|
201
|
+
if (!overrides)
|
|
202
|
+
return name;
|
|
203
|
+
return `${name}#${canonicalOverrideKey(overrides)}`;
|
|
204
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fusedio/widget-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "SDK for building custom json-ui components for the Fused workbench",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"typecheck": "tsc --noEmit",
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"bundle": "tsc --noEmit && esbuild src/index.ts --bundle --outfile=dist/bundle.js --format=esm --platform=browser --external:react --external:react/jsx-runtime --external:zod --minify",
|
|
24
|
+
"prepublishOnly": "bun run clean && bun run build && bun run bundle"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=18",
|
|
28
|
+
"zod": "^4.3.6"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"esbuild": "0.25.10",
|
|
32
|
+
"typescript": "^5.4.3",
|
|
33
|
+
"@types/react": "^18.3.11",
|
|
34
|
+
"zod": "^4.3.6"
|
|
35
|
+
}
|
|
36
|
+
}
|