@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,93 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
import type { z } from "zod";
|
|
3
|
+
import type { ComponentRenderProps } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* The shape a custom catalog registers per component type.
|
|
6
|
+
*
|
|
7
|
+
* Bundles default-export `{ "kebab-key": defineComponent({...}) }`. The
|
|
8
|
+
* workbench reads `component` into its rendering registry and reads
|
|
9
|
+
* `{ description, hasChildren, props }` into the **catalog schemas atom**
|
|
10
|
+
* so the AI surfaces (widget-builder, canvas main chat, auto-fix, inline
|
|
11
|
+
* edit) can see the type and the `get_json_ui_component_schemas` tool can
|
|
12
|
+
* resolve its propsSchema.
|
|
13
|
+
*
|
|
14
|
+
* The `props` Zod schema is constructed against the workbench's Zod
|
|
15
|
+
* instance at runtime: the catalog bundle marks `zod` as external, and the
|
|
16
|
+
* host's runtime import map resolves the bare `import { z } from "zod"`
|
|
17
|
+
* to its own already-loaded Zod. Your local devDependency on `zod` exists
|
|
18
|
+
* only so TypeScript can infer the prop type from the schema — no Zod
|
|
19
|
+
* code ships in your bundle.
|
|
20
|
+
*
|
|
21
|
+
* `props` must be a `z.ZodObject` (i.e. constructed with `z.object({...})`).
|
|
22
|
+
* The workbench's props linter calls `.strict().safeParse()` on it to
|
|
23
|
+
* validate widget JSON at the same fidelity as built-in components, and
|
|
24
|
+
* that method only exists on `ZodObject`. Bundles that pass `z.union`,
|
|
25
|
+
* `z.discriminatedUnion`, etc. are rejected at load time.
|
|
26
|
+
*/
|
|
27
|
+
export interface CatalogComponentDefinition<TProps extends Record<string, unknown> = Record<string, unknown>> {
|
|
28
|
+
/** React function component. Receives `{ element }` per the json-ui contract. */
|
|
29
|
+
component: React.ComponentType<ComponentRenderProps<TProps>>;
|
|
30
|
+
/**
|
|
31
|
+
* Zod schema for the component's props. Must be a `z.ZodObject` so the
|
|
32
|
+
* workbench's strict-mode linter can run; use `z.infer<typeof MySchema>`
|
|
33
|
+
* to derive the matching TS prop type and keep them in lockstep.
|
|
34
|
+
*/
|
|
35
|
+
props: z.ZodObject<z.ZodRawShape>;
|
|
36
|
+
/**
|
|
37
|
+
* One-line description shown to the AI in the system prompt's custom-catalog
|
|
38
|
+
* section. The AI uses this to decide when your component is appropriate;
|
|
39
|
+
* keep it action-oriented (e.g. "A counter that writes a number to a param").
|
|
40
|
+
*/
|
|
41
|
+
description?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Whether this component accepts nested `children` in the json-ui tree.
|
|
44
|
+
* Defaults to `false` — most catalog components are leaves.
|
|
45
|
+
*/
|
|
46
|
+
hasChildren?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Typed helper for registering a catalog component.
|
|
50
|
+
*
|
|
51
|
+
* `TProps` is inferred from the React component's props only — never from
|
|
52
|
+
* the Zod schema — because Zod 4's type instantiation through
|
|
53
|
+
* `z.ZodType<T>` exceeds TypeScript's recursion budget for moderately
|
|
54
|
+
* complex schemas. The author writes `type Props = z.infer<typeof
|
|
55
|
+
* MySchema>` once and uses it on the component signature.
|
|
56
|
+
*
|
|
57
|
+
* Tradeoff: since `props` is typed as `z.ZodObject<z.ZodRawShape>` (not
|
|
58
|
+
* `z.ZodType<TProps>`), TypeScript will *not* catch drift between the
|
|
59
|
+
* Zod schema's shape and the component's `Props`. If you change one,
|
|
60
|
+
* change the other — the runtime linter and renderer will surface the
|
|
61
|
+
* mismatch, but the build won't.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* import { z } from "zod";
|
|
65
|
+
* import { defineComponent, useFusedParam, type ComponentRenderProps } from "@fusedio/widget-sdk";
|
|
66
|
+
*
|
|
67
|
+
* const CounterButtonProps = z.object({
|
|
68
|
+
* param: z.string().describe("Canvas param key to read/write"),
|
|
69
|
+
* label: z.string().optional().describe("Display label"),
|
|
70
|
+
* step: z.number().optional().default(1),
|
|
71
|
+
* });
|
|
72
|
+
* type Props = z.infer<typeof CounterButtonProps>;
|
|
73
|
+
*
|
|
74
|
+
* function CounterButton({ element }: ComponentRenderProps<Props>) {
|
|
75
|
+
* const { param, label = "Count", step = 1 } = element.props;
|
|
76
|
+
* const { value, setValue } = useFusedParam({ param, defaultValue: 0 });
|
|
77
|
+
* return <button onClick={() => setValue(value + step)}>{label}: {value}</button>;
|
|
78
|
+
* }
|
|
79
|
+
*
|
|
80
|
+
* export default {
|
|
81
|
+
* "counter-button": defineComponent({
|
|
82
|
+
* component: CounterButton,
|
|
83
|
+
* props: CounterButtonProps,
|
|
84
|
+
* description: "A counter with +/- buttons that writes a number to a param.",
|
|
85
|
+
* }),
|
|
86
|
+
* };
|
|
87
|
+
*/
|
|
88
|
+
export declare function defineComponent<TProps extends Record<string, unknown>>(def: {
|
|
89
|
+
component: React.ComponentType<ComponentRenderProps<TProps>>;
|
|
90
|
+
props: z.ZodObject<z.ZodRawShape>;
|
|
91
|
+
description?: string;
|
|
92
|
+
hasChildren?: boolean;
|
|
93
|
+
}): CatalogComponentDefinition<TProps>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed helper for registering a catalog component.
|
|
3
|
+
*
|
|
4
|
+
* `TProps` is inferred from the React component's props only — never from
|
|
5
|
+
* the Zod schema — because Zod 4's type instantiation through
|
|
6
|
+
* `z.ZodType<T>` exceeds TypeScript's recursion budget for moderately
|
|
7
|
+
* complex schemas. The author writes `type Props = z.infer<typeof
|
|
8
|
+
* MySchema>` once and uses it on the component signature.
|
|
9
|
+
*
|
|
10
|
+
* Tradeoff: since `props` is typed as `z.ZodObject<z.ZodRawShape>` (not
|
|
11
|
+
* `z.ZodType<TProps>`), TypeScript will *not* catch drift between the
|
|
12
|
+
* Zod schema's shape and the component's `Props`. If you change one,
|
|
13
|
+
* change the other — the runtime linter and renderer will surface the
|
|
14
|
+
* mismatch, but the build won't.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* import { z } from "zod";
|
|
18
|
+
* import { defineComponent, useFusedParam, type ComponentRenderProps } from "@fusedio/widget-sdk";
|
|
19
|
+
*
|
|
20
|
+
* const CounterButtonProps = z.object({
|
|
21
|
+
* param: z.string().describe("Canvas param key to read/write"),
|
|
22
|
+
* label: z.string().optional().describe("Display label"),
|
|
23
|
+
* step: z.number().optional().default(1),
|
|
24
|
+
* });
|
|
25
|
+
* type Props = z.infer<typeof CounterButtonProps>;
|
|
26
|
+
*
|
|
27
|
+
* function CounterButton({ element }: ComponentRenderProps<Props>) {
|
|
28
|
+
* const { param, label = "Count", step = 1 } = element.props;
|
|
29
|
+
* const { value, setValue } = useFusedParam({ param, defaultValue: 0 });
|
|
30
|
+
* return <button onClick={() => setValue(value + step)}>{label}: {value}</button>;
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* export default {
|
|
34
|
+
* "counter-button": defineComponent({
|
|
35
|
+
* component: CounterButton,
|
|
36
|
+
* props: CounterButtonProps,
|
|
37
|
+
* description: "A counter with +/- buttons that writes a number to a param.",
|
|
38
|
+
* }),
|
|
39
|
+
* };
|
|
40
|
+
*/
|
|
41
|
+
export function defineComponent(def) {
|
|
42
|
+
return def;
|
|
43
|
+
}
|
package/dist/form.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription-based store for form field values. Notifies only subscribers
|
|
3
|
+
* watching the changed key so that slider dragging doesn't re-render every
|
|
4
|
+
* sibling.
|
|
5
|
+
*/
|
|
6
|
+
export interface FormParamsStore {
|
|
7
|
+
/** Read the current value for a single field name. */
|
|
8
|
+
get: (name: string) => unknown;
|
|
9
|
+
/** Read a snapshot containing only the requested names. */
|
|
10
|
+
getSnapshot: (names: readonly string[]) => Record<string, unknown>;
|
|
11
|
+
/** Read all field values (used by form submit). */
|
|
12
|
+
getAll: () => Record<string, unknown>;
|
|
13
|
+
/** Set a field value; notifies subscribers watching this name. */
|
|
14
|
+
setField: (name: string, value: unknown) => void;
|
|
15
|
+
/** Remove a field; notifies subscribers watching this name. */
|
|
16
|
+
removeField: (name: string) => void;
|
|
17
|
+
/** Subscribe to changes on any of the given names. Returns an unsubscribe fn. */
|
|
18
|
+
subscribe: (names: readonly string[], cb: () => void) => () => void;
|
|
19
|
+
}
|
|
20
|
+
/** Create a fresh form params store. Called once per Form component instance. */
|
|
21
|
+
export declare function createFormParamsStore(): FormParamsStore;
|
|
22
|
+
export interface FormContextValue {
|
|
23
|
+
/** Store holding the current field values for this form, or null when outside any form. */
|
|
24
|
+
store: FormParamsStore | null;
|
|
25
|
+
/** True when this subtree is rendered inside a Form component. */
|
|
26
|
+
isInForm: boolean;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Provides form-scoped values to all descendants. Provided by the workbench's
|
|
30
|
+
* built-in Form component and by the catalog-template test harness.
|
|
31
|
+
*/
|
|
32
|
+
export declare const FormContext: import("react").Context<FormContextValue>;
|
|
33
|
+
/** Read the form context — `{ store, isInForm }`. */
|
|
34
|
+
export declare function useFormContext(): FormContextValue;
|
|
35
|
+
/**
|
|
36
|
+
* Read live form-scoped values for the given param names.
|
|
37
|
+
*
|
|
38
|
+
* Returns `{ inForm: false, values: {} }` when used outside a form. Re-renders
|
|
39
|
+
* only when one of the watched names actually changes — not on every field
|
|
40
|
+
* update in the form.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const { inForm, values } = useFormParams(["city", "country"]);
|
|
44
|
+
* if (inForm && values.city) console.log("city:", values.city);
|
|
45
|
+
*/
|
|
46
|
+
export declare function useFormParams(names: readonly string[]): {
|
|
47
|
+
inForm: boolean;
|
|
48
|
+
values: Record<string, unknown>;
|
|
49
|
+
};
|
package/dist/form.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form-scoped parameter context.
|
|
3
|
+
*
|
|
4
|
+
* Provides a subscription store that mirrors live values of form fields
|
|
5
|
+
* (slider, dropdown, text-input, …) so that sibling components inside the
|
|
6
|
+
* same form can react to those values without ever broadcasting them to
|
|
7
|
+
* the canvas. The submit action is the only event that leaks values out.
|
|
8
|
+
*
|
|
9
|
+
* Pure React — no Jotai, no workbench dependencies. The workbench's Form
|
|
10
|
+
* component and the catalog-template test harness both call
|
|
11
|
+
* `createFormParamsStore()` and provide it via `FormContext`.
|
|
12
|
+
*/
|
|
13
|
+
import { createContext, useCallback, useContext, useRef, useSyncExternalStore, } from "react";
|
|
14
|
+
/** Create a fresh form params store. Called once per Form component instance. */
|
|
15
|
+
export function createFormParamsStore() {
|
|
16
|
+
const values = new Map();
|
|
17
|
+
const subscribers = new Set();
|
|
18
|
+
const notify = (name) => {
|
|
19
|
+
subscribers.forEach((sub) => {
|
|
20
|
+
if (sub.names.has(name))
|
|
21
|
+
sub.cb();
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
get(name) {
|
|
26
|
+
return values.get(name);
|
|
27
|
+
},
|
|
28
|
+
getSnapshot(names) {
|
|
29
|
+
const out = {};
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
if (values.has(name))
|
|
32
|
+
out[name] = values.get(name);
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
},
|
|
36
|
+
getAll() {
|
|
37
|
+
const out = {};
|
|
38
|
+
values.forEach((v, k) => {
|
|
39
|
+
out[k] = v;
|
|
40
|
+
});
|
|
41
|
+
return out;
|
|
42
|
+
},
|
|
43
|
+
setField(name, value) {
|
|
44
|
+
if (values.has(name) && Object.is(values.get(name), value))
|
|
45
|
+
return;
|
|
46
|
+
values.set(name, value);
|
|
47
|
+
notify(name);
|
|
48
|
+
},
|
|
49
|
+
removeField(name) {
|
|
50
|
+
if (!values.has(name))
|
|
51
|
+
return;
|
|
52
|
+
values.delete(name);
|
|
53
|
+
notify(name);
|
|
54
|
+
},
|
|
55
|
+
subscribe(names, cb) {
|
|
56
|
+
const sub = { names: new Set(names), cb };
|
|
57
|
+
subscribers.add(sub);
|
|
58
|
+
return () => {
|
|
59
|
+
subscribers.delete(sub);
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Provides form-scoped values to all descendants. Provided by the workbench's
|
|
66
|
+
* built-in Form component and by the catalog-template test harness.
|
|
67
|
+
*/
|
|
68
|
+
export const FormContext = createContext({
|
|
69
|
+
store: null,
|
|
70
|
+
isInForm: false,
|
|
71
|
+
});
|
|
72
|
+
FormContext.displayName = "JsonUiFormContext";
|
|
73
|
+
/** Read the form context — `{ store, isInForm }`. */
|
|
74
|
+
export function useFormContext() {
|
|
75
|
+
return useContext(FormContext);
|
|
76
|
+
}
|
|
77
|
+
// ============================================================================
|
|
78
|
+
// Hook: read form-scoped params
|
|
79
|
+
// ============================================================================
|
|
80
|
+
const EMPTY_PARAMS = Object.freeze({});
|
|
81
|
+
/**
|
|
82
|
+
* Read live form-scoped values for the given param names.
|
|
83
|
+
*
|
|
84
|
+
* Returns `{ inForm: false, values: {} }` when used outside a form. Re-renders
|
|
85
|
+
* only when one of the watched names actually changes — not on every field
|
|
86
|
+
* update in the form.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* const { inForm, values } = useFormParams(["city", "country"]);
|
|
90
|
+
* if (inForm && values.city) console.log("city:", values.city);
|
|
91
|
+
*/
|
|
92
|
+
export function useFormParams(names) {
|
|
93
|
+
const { store, isInForm } = useFormContext();
|
|
94
|
+
const stableNames = useStableStringArray(names);
|
|
95
|
+
const subscribe = useCallback((cb) => {
|
|
96
|
+
if (!store)
|
|
97
|
+
return () => { };
|
|
98
|
+
return store.subscribe(stableNames, cb);
|
|
99
|
+
}, [store, stableNames]);
|
|
100
|
+
const snapshotRef = useRef(EMPTY_PARAMS);
|
|
101
|
+
const getSnapshot = useCallback(() => {
|
|
102
|
+
if (!store)
|
|
103
|
+
return EMPTY_PARAMS;
|
|
104
|
+
const next = store.getSnapshot(stableNames);
|
|
105
|
+
const prev = snapshotRef.current;
|
|
106
|
+
if (shallowEqualRecords(prev, next))
|
|
107
|
+
return prev;
|
|
108
|
+
snapshotRef.current = next;
|
|
109
|
+
return next;
|
|
110
|
+
}, [store, stableNames]);
|
|
111
|
+
const values = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
112
|
+
return { inForm: isInForm, values };
|
|
113
|
+
}
|
|
114
|
+
function shallowEqualRecords(a, b) {
|
|
115
|
+
if (a === b)
|
|
116
|
+
return true;
|
|
117
|
+
const keysA = Object.keys(a);
|
|
118
|
+
const keysB = Object.keys(b);
|
|
119
|
+
if (keysA.length !== keysB.length)
|
|
120
|
+
return false;
|
|
121
|
+
for (const k of keysA) {
|
|
122
|
+
if (!Object.is(a[k], b[k]))
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
/** Return a stable array reference when the string contents don't change. */
|
|
128
|
+
function useStableStringArray(names) {
|
|
129
|
+
const ref = useRef(names);
|
|
130
|
+
const prev = ref.current;
|
|
131
|
+
if (prev !== names &&
|
|
132
|
+
(prev.length !== names.length || prev.some((n, i) => n !== names[i]))) {
|
|
133
|
+
ref.current = names;
|
|
134
|
+
}
|
|
135
|
+
return ref.current;
|
|
136
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AllowedSource } from "../bridge";
|
|
2
|
+
/**
|
|
3
|
+
* Returns the set of UDFs allowed to broadcast params to the current node,
|
|
4
|
+
* computed from canvas edges.
|
|
5
|
+
*
|
|
6
|
+
* Returns `{ allowedSources: null }` when no filtering is applied — typically
|
|
7
|
+
* because the node has no identity or runs in a shared/embed context where
|
|
8
|
+
* topology isn't available.
|
|
9
|
+
*
|
|
10
|
+
* The companion `isAllowedSource(originUdfId, originUdfName)` helper checks
|
|
11
|
+
* whether a specific source identity is permitted.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* const { allowedSources, isAllowedSource } = useAllowedSources();
|
|
15
|
+
* console.log("allowed:", allowedSources?.map(s => s.udfName));
|
|
16
|
+
*/
|
|
17
|
+
export declare function useAllowedSources(): {
|
|
18
|
+
allowedSources: ReadonlyArray<AllowedSource> | null;
|
|
19
|
+
isAllowedSource: (originUdfId?: string, originUdfName?: string) => boolean;
|
|
20
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
3
|
+
/**
|
|
4
|
+
* Returns the set of UDFs allowed to broadcast params to the current node,
|
|
5
|
+
* computed from canvas edges.
|
|
6
|
+
*
|
|
7
|
+
* Returns `{ allowedSources: null }` when no filtering is applied — typically
|
|
8
|
+
* because the node has no identity or runs in a shared/embed context where
|
|
9
|
+
* topology isn't available.
|
|
10
|
+
*
|
|
11
|
+
* The companion `isAllowedSource(originUdfId, originUdfName)` helper checks
|
|
12
|
+
* whether a specific source identity is permitted.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const { allowedSources, isAllowedSource } = useAllowedSources();
|
|
16
|
+
* console.log("allowed:", allowedSources?.map(s => s.udfName));
|
|
17
|
+
*/
|
|
18
|
+
export function useAllowedSources() {
|
|
19
|
+
const bridge = useFusedWidgetBridge();
|
|
20
|
+
const subscribe = useCallback((cb) => bridge.routing.subscribeAllowedSources(cb), [bridge]);
|
|
21
|
+
const snapshotRef = useRef(null);
|
|
22
|
+
const getSnapshot = useCallback(() => {
|
|
23
|
+
const next = bridge.routing.getAllowedSources();
|
|
24
|
+
const prev = snapshotRef.current;
|
|
25
|
+
if (allowedSourcesEqual(prev, next))
|
|
26
|
+
return prev;
|
|
27
|
+
snapshotRef.current = next;
|
|
28
|
+
return next;
|
|
29
|
+
}, [bridge]);
|
|
30
|
+
const allowedSources = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
31
|
+
const isAllowedSource = useCallback((originUdfId, originUdfName) => {
|
|
32
|
+
if (!allowedSources || allowedSources.length === 0)
|
|
33
|
+
return true;
|
|
34
|
+
if (!originUdfId && !originUdfName)
|
|
35
|
+
return true;
|
|
36
|
+
return allowedSources.some((s) => (s.udfUniqueId && s.udfUniqueId === originUdfId) ||
|
|
37
|
+
(s.udfName && s.udfName === originUdfName));
|
|
38
|
+
}, [allowedSources]);
|
|
39
|
+
return { allowedSources, isAllowedSource };
|
|
40
|
+
}
|
|
41
|
+
function allowedSourcesEqual(a, b) {
|
|
42
|
+
if (a === b)
|
|
43
|
+
return true;
|
|
44
|
+
if (a === null || b === null)
|
|
45
|
+
return false;
|
|
46
|
+
if (a.length !== b.length)
|
|
47
|
+
return false;
|
|
48
|
+
return a.every((src, i) => src.udfUniqueId === b[i].udfUniqueId && src.udfName === b[i].udfName);
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the set of UDF names this node may reference (computed from
|
|
3
|
+
* incoming edges). Returns `null` when no filtering applies (e.g. node has
|
|
4
|
+
* no identity, or running in a shared widget context).
|
|
5
|
+
*
|
|
6
|
+
* Use this to gate AI suggestions or SQL queries to only reference
|
|
7
|
+
* connected UDFs — ensures consistency with the canvas's edge topology.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const allowed = useAllowedUdfNames();
|
|
11
|
+
* if (allowed && !allowed.has(udfName)) {
|
|
12
|
+
* return <div>UDF "{udfName}" is not reachable from this node.</div>;
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export declare function useAllowedUdfNames(): Set<string> | null;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
3
|
+
/**
|
|
4
|
+
* Returns the set of UDF names this node may reference (computed from
|
|
5
|
+
* incoming edges). Returns `null` when no filtering applies (e.g. node has
|
|
6
|
+
* no identity, or running in a shared widget context).
|
|
7
|
+
*
|
|
8
|
+
* Use this to gate AI suggestions or SQL queries to only reference
|
|
9
|
+
* connected UDFs — ensures consistency with the canvas's edge topology.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const allowed = useAllowedUdfNames();
|
|
13
|
+
* if (allowed && !allowed.has(udfName)) {
|
|
14
|
+
* return <div>UDF "{udfName}" is not reachable from this node.</div>;
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
export function useAllowedUdfNames() {
|
|
18
|
+
const bridge = useFusedWidgetBridge();
|
|
19
|
+
const subscribe = useCallback((cb) => bridge.routing.subscribeAllowedSources(cb), [bridge]);
|
|
20
|
+
const snapshotRef = useRef(null);
|
|
21
|
+
const getSnapshot = useCallback(() => {
|
|
22
|
+
const next = bridge.routing.getAllowedUdfNames();
|
|
23
|
+
const prev = snapshotRef.current;
|
|
24
|
+
if (setsEqual(prev, next))
|
|
25
|
+
return prev;
|
|
26
|
+
snapshotRef.current = next;
|
|
27
|
+
return next;
|
|
28
|
+
}, [bridge]);
|
|
29
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
30
|
+
}
|
|
31
|
+
function setsEqual(a, b) {
|
|
32
|
+
if (a === b)
|
|
33
|
+
return true;
|
|
34
|
+
if (a === null || b === null)
|
|
35
|
+
return false;
|
|
36
|
+
if (a.size !== b.size)
|
|
37
|
+
return false;
|
|
38
|
+
for (const x of a)
|
|
39
|
+
if (!b.has(x))
|
|
40
|
+
return false;
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read filtered canvas parameter values for multiple param names at once.
|
|
3
|
+
*
|
|
4
|
+
* The returned values are filtered by the current node's incoming edges —
|
|
5
|
+
* only params broadcast by upstream-connected nodes are visible.
|
|
6
|
+
*
|
|
7
|
+
* Re-renders only when one of the watched params actually changes.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* const { city, year } = useCanvasParams(["city", "year"]);
|
|
11
|
+
* if (city) console.log("city is", city);
|
|
12
|
+
*/
|
|
13
|
+
export declare function useCanvasParams(paramNames: string[]): Record<string, unknown>;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { useCallback, useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { useFusedWidgetBridge } from "../bridge";
|
|
3
|
+
const EMPTY_RECORD = Object.freeze({});
|
|
4
|
+
/**
|
|
5
|
+
* Read filtered canvas parameter values for multiple param names at once.
|
|
6
|
+
*
|
|
7
|
+
* The returned values are filtered by the current node's incoming edges —
|
|
8
|
+
* only params broadcast by upstream-connected nodes are visible.
|
|
9
|
+
*
|
|
10
|
+
* Re-renders only when one of the watched params actually changes.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* const { city, year } = useCanvasParams(["city", "year"]);
|
|
14
|
+
* if (city) console.log("city is", city);
|
|
15
|
+
*/
|
|
16
|
+
export function useCanvasParams(paramNames) {
|
|
17
|
+
const bridge = useFusedWidgetBridge();
|
|
18
|
+
const stableNames = useStableStringArray(paramNames);
|
|
19
|
+
const subscribe = useCallback((cb) => {
|
|
20
|
+
if (stableNames.length === 0)
|
|
21
|
+
return () => { };
|
|
22
|
+
return bridge.params.subscribeMany(stableNames, cb);
|
|
23
|
+
}, [bridge, stableNames]);
|
|
24
|
+
const snapshotRef = useRef(EMPTY_RECORD);
|
|
25
|
+
const getSnapshot = useCallback(() => {
|
|
26
|
+
if (stableNames.length === 0)
|
|
27
|
+
return EMPTY_RECORD;
|
|
28
|
+
const next = bridge.params.getSnapshotMany(stableNames);
|
|
29
|
+
const prev = snapshotRef.current;
|
|
30
|
+
if (shallowEqualRecords(prev, next))
|
|
31
|
+
return prev;
|
|
32
|
+
snapshotRef.current = next;
|
|
33
|
+
return next;
|
|
34
|
+
}, [bridge, stableNames]);
|
|
35
|
+
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
36
|
+
}
|
|
37
|
+
function shallowEqualRecords(a, b) {
|
|
38
|
+
if (a === b)
|
|
39
|
+
return true;
|
|
40
|
+
const keysA = Object.keys(a);
|
|
41
|
+
const keysB = Object.keys(b);
|
|
42
|
+
if (keysA.length !== keysB.length)
|
|
43
|
+
return false;
|
|
44
|
+
for (const k of keysA) {
|
|
45
|
+
if (!Object.is(a[k], b[k]))
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
function useStableStringArray(names) {
|
|
51
|
+
const ref = useRef(names);
|
|
52
|
+
const prev = ref.current;
|
|
53
|
+
if (prev !== names &&
|
|
54
|
+
(prev.length !== names.length || prev.some((n, i) => n !== names[i]))) {
|
|
55
|
+
ref.current = names;
|
|
56
|
+
}
|
|
57
|
+
return ref.current;
|
|
58
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export interface UseDuckDbSqlQueryOptions {
|
|
2
|
+
/**
|
|
3
|
+
* SQL query with `{{udf_name}}` and `$param_name` placeholders.
|
|
4
|
+
* Example: `"SELECT DISTINCT city FROM {{my_udf}} ORDER BY city"`
|
|
5
|
+
*/
|
|
6
|
+
sql?: string;
|
|
7
|
+
/** When false, skip preprocessing and execution. */
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Safety LIMIT appended if the SQL doesn't include one. Defaults to 500;
|
|
11
|
+
* map widgets pass 100_000.
|
|
12
|
+
*/
|
|
13
|
+
maxRows?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Workbench-only: override `{{name}}` placeholders with in-memory
|
|
16
|
+
* relations (DuckDB tabs in sql-runner/code-editor). When `relationName`
|
|
17
|
+
* is provided, the placeholder is replaced with `"<relationName>"` rather
|
|
18
|
+
* than the VFS filename. Other hosts pass `undefined`.
|
|
19
|
+
*/
|
|
20
|
+
sourceOverrides?: Record<string, {
|
|
21
|
+
relationName: string;
|
|
22
|
+
error?: string;
|
|
23
|
+
loading?: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
/** @deprecated Use `maxRows` — kept temporarily for the SDK's existing surface. */
|
|
26
|
+
defaultLimit?: number;
|
|
27
|
+
}
|
|
28
|
+
export interface UseDuckDbSqlQueryResult {
|
|
29
|
+
rows: ReadonlyArray<Record<string, unknown>>;
|
|
30
|
+
columns: readonly string[];
|
|
31
|
+
loading: boolean;
|
|
32
|
+
/** Legacy: components expect `error: string | null`. */
|
|
33
|
+
error: string | null;
|
|
34
|
+
refetch: () => void;
|
|
35
|
+
}
|
|
36
|
+
export interface UseDuckDbSqlQueryPreprocessingResult {
|
|
37
|
+
processedSql: string;
|
|
38
|
+
loading: boolean;
|
|
39
|
+
error: string | null;
|
|
40
|
+
refetch: () => void;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Preprocess SQL: resolve placeholders, register UDFs via the bridge,
|
|
44
|
+
* substitute params, sign URLs, append LIMIT. Returns the prepared SQL.
|
|
45
|
+
*/
|
|
46
|
+
export declare function useDuckDbSqlQueryPreprocessing({ sql, enabled, maxRows, sourceOverrides, }: UseDuckDbSqlQueryOptions): UseDuckDbSqlQueryPreprocessingResult;
|
|
47
|
+
/**
|
|
48
|
+
* Execute a DuckDB SQL query against UDF Parquet outputs. Uses
|
|
49
|
+
* `useDuckDbSqlQueryPreprocessing` to prepare the SQL string, then runs
|
|
50
|
+
* it via the bridge.
|
|
51
|
+
*/
|
|
52
|
+
export declare function useDuckDbSqlQuery({ sql, enabled, maxRows, sourceOverrides, }: UseDuckDbSqlQueryOptions): UseDuckDbSqlQueryResult;
|
|
53
|
+
/**
|
|
54
|
+
* Resolve UDF names to VFS filenames, registering them in DuckDB if needed.
|
|
55
|
+
* Exposed for advanced use cases (e.g. building your own query string).
|
|
56
|
+
*/
|
|
57
|
+
export declare function useVfsRegistration(udfNames: readonly string[], enabled?: boolean): {
|
|
58
|
+
filenames: Map<string, string>;
|
|
59
|
+
loading: boolean;
|
|
60
|
+
error?: string;
|
|
61
|
+
};
|