@altertable/data-app 0.61.0 → 0.62.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/AGENTS.md +5 -3
- package/CONTRIBUTING.md +7 -4
- package/README.md +14 -14
- package/dist/chunks/{contract-ehk50k7e.js → contract-3bnrf5pf.js} +25 -3
- package/dist/chunks/contract-3bnrf5pf.js.map +10 -0
- package/dist/chunks/{contract-zr4s2g7m.js → contract-4vrw9zk9.js} +41 -11
- package/dist/chunks/contract-4vrw9zk9.js.map +12 -0
- package/dist/chunks/{contract-14vxdcrs.js → contract-farfe948.js} +17 -21
- package/dist/chunks/contract-farfe948.js.map +10 -0
- package/dist/chunks/contract-nt819swq.js.map +1 -1
- package/dist/chunks/{contract-7gpsee0v.js → contract-rrm7s5zp.js} +48 -19
- package/dist/chunks/contract-rrm7s5zp.js.map +12 -0
- package/dist/chunks/{contract-d9skd1n7.js → contract-xjv197ck.js} +161 -14
- package/dist/chunks/contract-xjv197ck.js.map +13 -0
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/core/appearance.js +1 -1
- package/dist/core/contract.js +5 -3
- package/dist/core/contract.js.map +1 -1
- package/dist/embed/index.js +42 -8
- package/dist/embed/index.js.map +5 -4
- package/dist/local.js +191 -79
- package/dist/local.js.map +8 -7
- package/dist/react/embed/index.js +84 -99
- package/dist/react/embed/index.js.map +4 -5
- package/dist/react/index.css +156 -6
- package/dist/react/index.js +313 -152
- package/dist/react/index.js.map +17 -13
- package/dist/server.js +172 -76
- package/dist/server.js.map +7 -6
- package/dist/types/client/iframe.d.ts +14 -0
- package/dist/types/client/index.d.ts +19 -13
- package/dist/types/core/appearance.d.ts +7 -6
- package/dist/types/core/contract.d.ts +3 -3
- package/dist/types/core/messages.d.ts +7 -1
- package/dist/types/core/operation.d.ts +27 -0
- package/dist/types/core/presentation.d.ts +7 -0
- package/dist/types/embed/bridge.d.ts +12 -0
- package/dist/types/embed/host.d.ts +10 -3
- package/dist/types/embed/index.d.ts +7 -4
- package/dist/types/embed/{shell.d.ts → source.d.ts} +5 -4
- package/dist/types/embed/sql.d.ts +4 -0
- package/dist/types/react/embed/bridge.d.ts +19 -10
- package/dist/types/react/embed/index.d.ts +0 -2
- package/dist/types/react/ui/DataAppSkeleton.d.ts +8 -0
- package/dist/types/react/ui/SearchInput.d.ts +1 -1
- package/dist/types/react/ui/index.d.ts +2 -0
- package/dist/types/react/ui/useAppAppearance.d.ts +3 -0
- package/dist/types/react/ui/useDataAppPresentation.d.ts +3 -0
- package/dist/types/react/view-controls.d.ts +1 -1
- package/dist/{bootstrap.js → worker.js} +144 -12
- package/docs/app-authoring.md +24 -4
- package/docs/appearance.md +21 -8
- package/docs/client.md +39 -2
- package/docs/contract.md +2 -2
- package/docs/embed.md +105 -18
- package/docs/react-embed.md +84 -24
- package/docs/react.md +51 -0
- package/docs/server-bun.md +5 -0
- package/docs/worker.md +57 -0
- package/package.json +3 -3
- package/dist/chunks/contract-14vxdcrs.js.map +0 -10
- package/dist/chunks/contract-7gpsee0v.js.map +0 -11
- package/dist/chunks/contract-d9skd1n7.js.map +0 -12
- package/dist/chunks/contract-ehk50k7e.js.map +0 -10
- package/dist/chunks/contract-zr4s2g7m.js.map +0 -12
- package/dist/types/embed/standalone.d.ts +0 -1
- package/dist/types/react/embed/shell.d.ts +0 -10
- package/docs/bootstrap.md +0 -76
|
@@ -1,13 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* @module @altertable/data-app/client
|
|
4
|
-
* @see https://github.com/altertable-ai/data-app/blob/main/docs/client.md
|
|
5
|
-
*/
|
|
6
|
-
import { type DataTransport } from './transport.js';
|
|
7
|
-
export { createHttpTransport, DataAppError, type DataTransport, } from './transport.js';
|
|
8
|
-
export { createIframeTransport, installDataAppTransport, getDataAppTransport, type IframeTransport, } from './iframe.js';
|
|
9
|
-
export { createMessageClient } from './messages.js';
|
|
10
|
-
import type { DataOperations, DisclosedQuery } from '../core/contract.js';
|
|
1
|
+
import type { DataOperations, DisclosedQuery, Lakehouse } from '../core/contract.js';
|
|
2
|
+
import type { DataTransport } from './transport.js';
|
|
11
3
|
export type InputOf<T> = T extends {
|
|
12
4
|
input: (value: unknown) => infer Input;
|
|
13
5
|
} ? Input : never;
|
|
@@ -32,11 +24,25 @@ export type DataClient<Operations extends DataOperations> = {
|
|
|
32
24
|
signal?: AbortSignal;
|
|
33
25
|
}): Promise<DataResponse<OutputOf<Operations[Name]>, InputOf<Operations[Name]>>>;
|
|
34
26
|
};
|
|
35
|
-
/** Browser
|
|
36
|
-
export
|
|
27
|
+
/** Browser operations and named-operation delivery are mutually exclusive configurations. */
|
|
28
|
+
export type DataClientOptions<Operations extends DataOperations> = {
|
|
29
|
+
operations: Operations;
|
|
30
|
+
lakehouse?: Lakehouse;
|
|
31
|
+
transport?: never;
|
|
32
|
+
endpoint?: never;
|
|
33
|
+
fetch?: never;
|
|
34
|
+
} | {
|
|
35
|
+
operations?: never;
|
|
36
|
+
lakehouse?: never;
|
|
37
37
|
transport?: DataTransport;
|
|
38
38
|
endpoint?: string;
|
|
39
39
|
fetch?: typeof fetch;
|
|
40
|
-
}
|
|
40
|
+
};
|
|
41
|
+
/** Named HTTP operations, or browser-owned operations executed through an authorized SQL bridge. */
|
|
42
|
+
export declare function createDataClient<Operations extends DataOperations>(options?: DataClientOptions<Operations>): DataClient<Operations>;
|
|
41
43
|
export type { DataAppLocation, AppLocation } from './location.js';
|
|
42
44
|
export { createDataAppNavigation, getDataAppNavigation, type DataAppNavigation, } from './navigation.js';
|
|
45
|
+
export { createHttpTransport, DataAppError, type DataTransport, } from './transport.js';
|
|
46
|
+
export { createIframeTransport, installDataAppTransport, getDataAppTransport, type IframeTransport, } from './iframe.js';
|
|
47
|
+
export { createMessageClient } from './messages.js';
|
|
48
|
+
export type { DataAppPresentation } from '../core/presentation.js';
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
export type Theme = 'light' | 'dark';
|
|
2
|
+
export type ThemePreference = Theme | 'system';
|
|
1
3
|
export type AppearanceSettings = {
|
|
2
|
-
|
|
4
|
+
theme: ThemePreference;
|
|
3
5
|
baseColor: 'neutral' | 'slate' | 'warm';
|
|
4
6
|
accentColor: string;
|
|
5
7
|
darkAccentColor?: string;
|
|
@@ -12,10 +14,9 @@ export type AppearanceSettings = {
|
|
|
12
14
|
heading: string;
|
|
13
15
|
};
|
|
14
16
|
};
|
|
15
|
-
export type ThemeMode = AppearanceSettings['mode'];
|
|
16
17
|
export type ThemeController = {
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
getTheme: () => ThemePreference;
|
|
19
|
+
setTheme: (theme: ThemePreference) => void;
|
|
19
20
|
subscribe: (listener: () => void) => () => void;
|
|
20
21
|
};
|
|
21
22
|
export declare function parseAppearance(value: unknown): AppearanceSettings;
|
|
@@ -24,5 +25,5 @@ export declare function parseAppearance(value: unknown): AppearanceSettings;
|
|
|
24
25
|
* system-theme listening.
|
|
25
26
|
*/
|
|
26
27
|
export declare function applyAppearance(value: unknown): () => void;
|
|
27
|
-
/**
|
|
28
|
-
export declare function createThemeController(
|
|
28
|
+
/** Persist a viewer theme preference. Applying appearance belongs to the UI owner. */
|
|
29
|
+
export declare function createThemeController(initialTheme?: ThemePreference): ThemeController;
|
|
@@ -80,7 +80,7 @@ export declare class DataSourceError extends Error {
|
|
|
80
80
|
queryName?: string;
|
|
81
81
|
constructor(reason: 'unauthorized' | 'forbidden' | 'rate_limited' | 'query_rejected' | 'unavailable', status?: number | undefined);
|
|
82
82
|
}
|
|
83
|
-
/**
|
|
83
|
+
/** Query interface supplied by a server adapter or an authorized iframe bridge. */
|
|
84
84
|
export type Lakehouse = {
|
|
85
85
|
queryAll(statement: string, options: {
|
|
86
86
|
limit: number;
|
|
@@ -93,8 +93,8 @@ export type OperationContext = {
|
|
|
93
93
|
signal: AbortSignal;
|
|
94
94
|
};
|
|
95
95
|
/**
|
|
96
|
-
*
|
|
97
|
-
*
|
|
96
|
+
* Parsers run in the operation executor's runtime: server for HTTP apps, browser for bundle apps.
|
|
97
|
+
* Browser validation does not replace backend authorization or query limits.
|
|
98
98
|
*/
|
|
99
99
|
export type DataOperation<Input, Output> = {
|
|
100
100
|
input: (value: unknown) => Input;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TransportResponse } from './bridge.js';
|
|
2
|
-
import type { DisclosedQuery } from './contract.js';
|
|
2
|
+
import type { DisclosedQuery, QueryResult } from './contract.js';
|
|
3
3
|
export type MessageContext = {
|
|
4
4
|
signal: AbortSignal;
|
|
5
5
|
};
|
|
@@ -70,6 +70,12 @@ export type NavigationUpdate = {
|
|
|
70
70
|
title?: string;
|
|
71
71
|
};
|
|
72
72
|
export declare const navigationUpdateRoute: MessageRoute<NavigationUpdate, null>;
|
|
73
|
+
export type SqlQueryInput = {
|
|
74
|
+
statement: string;
|
|
75
|
+
limit: number;
|
|
76
|
+
};
|
|
77
|
+
/** SQL delivery for browser-owned operations. Hosts must enforce backend access and resource limits. */
|
|
78
|
+
export declare const sqlQueryRoute: MessageRoute<SqlQueryInput, QueryResult>;
|
|
73
79
|
export declare const dataAppRoutes: {
|
|
74
80
|
'data:query': DataQueryRoute<OperationContracts>;
|
|
75
81
|
'navigation:update': MessageRoute<NavigationUpdate, null>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type DataOperations, type DisclosedQuery, type Lakehouse } from './contract.js';
|
|
2
|
+
type Operation = DataOperations[string];
|
|
3
|
+
type OperationExecutionOptions = {
|
|
4
|
+
operationName: string;
|
|
5
|
+
lakehouse: Lakehouse;
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
/** Include query evidence in the response; this does not keep browser-owned SQL private. */
|
|
8
|
+
includeSql: boolean;
|
|
9
|
+
requestId: string;
|
|
10
|
+
};
|
|
11
|
+
/** Shared operation execution. Adapters own authorization, delivery, and public errors. */
|
|
12
|
+
export declare function executeDataOperation(operation: Operation, value: unknown, { operationName, lakehouse, signal: requestSignal, includeSql, requestId, }: OperationExecutionOptions): Promise<{
|
|
13
|
+
body: {
|
|
14
|
+
data: unknown;
|
|
15
|
+
requestId: string;
|
|
16
|
+
queriedAt: string;
|
|
17
|
+
queryIds: string[];
|
|
18
|
+
queries?: DisclosedQuery[] | undefined;
|
|
19
|
+
};
|
|
20
|
+
serializedBody: string;
|
|
21
|
+
}>;
|
|
22
|
+
export declare function toDataOperationFailure(error: unknown, aborted?: boolean): {
|
|
23
|
+
status: number;
|
|
24
|
+
code: string;
|
|
25
|
+
message: string;
|
|
26
|
+
};
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Theme } from './appearance.js';
|
|
2
|
+
/** Presentation owned by the parent shell, delivered only over a trusted bridge. */
|
|
3
|
+
export type DataAppPresentation = {
|
|
4
|
+
surface: 'embedded' | 'standalone';
|
|
5
|
+
theme: Theme;
|
|
6
|
+
};
|
|
7
|
+
export declare function isDataAppPresentation(value: unknown): value is DataAppPresentation;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type DataAppConnectionOptions } from './host.js';
|
|
2
|
+
import { type DataAppSourceOptions } from './source.js';
|
|
3
|
+
export type DataAppBridgeOptions = (DataAppSourceOptions & {
|
|
4
|
+
connection?: never;
|
|
5
|
+
window?: never;
|
|
6
|
+
javascript?: never;
|
|
7
|
+
}) | (DataAppConnectionOptions & {
|
|
8
|
+
source?: never;
|
|
9
|
+
startupTimeoutMs?: never;
|
|
10
|
+
});
|
|
11
|
+
/** Source mode configures and loads the iframe; connection mode attaches to a host-owned document. */
|
|
12
|
+
export declare function attachDataAppBridge(options: DataAppBridgeOptions): import("./host.js").DataAppHost;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DataAppPresentation } from '../core/presentation.js';
|
|
1
2
|
import { type MessageDispatcher } from '../core/messages.js';
|
|
2
3
|
export type DataAppConnection = {
|
|
3
4
|
type: 'origin';
|
|
@@ -11,13 +12,19 @@ export type DataAppDiagnostic = {
|
|
|
11
12
|
direction: 'send' | 'receive';
|
|
12
13
|
type: string;
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
export type DataAppHost = {
|
|
16
|
+
dispose: () => void;
|
|
17
|
+
setPresentation: (presentation?: DataAppPresentation) => void;
|
|
18
|
+
};
|
|
19
|
+
export type DataAppConnectionOptions = {
|
|
16
20
|
iframe: HTMLIFrameElement;
|
|
17
21
|
connection: DataAppConnection;
|
|
18
22
|
javascript?: string;
|
|
23
|
+
presentation?: DataAppPresentation;
|
|
19
24
|
onStatusChange?: (status: DataAppStatus) => void;
|
|
20
25
|
onDiagnostic?: (event: DataAppDiagnostic) => void;
|
|
21
26
|
onMessage: MessageDispatcher;
|
|
22
27
|
window?: Window;
|
|
23
|
-
}
|
|
28
|
+
};
|
|
29
|
+
/** The bridge owns delivery and cancellation; the routed handler owns validation, authorization and execution. */
|
|
30
|
+
export declare function attachDataAppConnection({ iframe, connection, javascript, presentation, onStatusChange, onDiagnostic, onMessage, window: host, }: DataAppConnectionOptions): DataAppHost;
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
* @module @altertable/data-app/embed
|
|
4
4
|
* @see https://github.com/altertable-ai/data-app/blob/main/docs/embed.md
|
|
5
5
|
*/
|
|
6
|
-
export { attachDataAppBridge } from './
|
|
7
|
-
export type {
|
|
8
|
-
export {
|
|
9
|
-
export type { DataAppSource
|
|
6
|
+
export { attachDataAppBridge } from './bridge.js';
|
|
7
|
+
export type { DataAppBridgeOptions } from './bridge.js';
|
|
8
|
+
export type { DataAppHost, DataAppConnection, DataAppStatus, DataAppDiagnostic, } from './host.js';
|
|
9
|
+
export type { DataAppSource } from './source.js';
|
|
10
10
|
export { startDataAppBootstrap } from './bootstrap.js';
|
|
11
11
|
export { createNavigationHandler } from './navigation.js';
|
|
12
|
+
export { sqlQueryRoute, type SqlQueryInput } from '../core/messages.js';
|
|
13
|
+
export { createSqlQueryHandler } from './sql.js';
|
|
14
|
+
export type { DataAppPresentation } from '../core/presentation.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { DataAppPresentation } from '../core/presentation.js';
|
|
1
2
|
import type { MessageDispatcher } from '../core/messages.js';
|
|
2
|
-
import { type DataAppStatus, type DataAppDiagnostic } from './host.js';
|
|
3
|
+
import { type DataAppStatus, type DataAppHost, type DataAppDiagnostic } from './host.js';
|
|
3
4
|
export type DataAppSource = {
|
|
4
5
|
type: 'url';
|
|
5
6
|
url: string;
|
|
@@ -7,15 +8,15 @@ export type DataAppSource = {
|
|
|
7
8
|
type: 'bundle';
|
|
8
9
|
bootstrapUrl: string;
|
|
9
10
|
javascript: string;
|
|
10
|
-
revision: string;
|
|
11
11
|
};
|
|
12
|
-
export type
|
|
12
|
+
export type DataAppSourceOptions = {
|
|
13
13
|
iframe: HTMLIFrameElement;
|
|
14
14
|
source: DataAppSource;
|
|
15
|
+
presentation?: DataAppPresentation;
|
|
15
16
|
onMessage: MessageDispatcher;
|
|
16
17
|
onStatusChange?: (status: DataAppStatus) => void;
|
|
17
18
|
onDiagnostic?: (event: DataAppDiagnostic) => void;
|
|
18
19
|
startupTimeoutMs?: number;
|
|
19
20
|
};
|
|
20
21
|
/** Owns loading and sandbox policy. Dispose before replacing the source or retrying. */
|
|
21
|
-
export declare function
|
|
22
|
+
export declare function attachDataAppSource({ iframe, source, presentation, onMessage, onStatusChange, onDiagnostic, startupTimeoutMs, }: DataAppSourceOptions): DataAppHost;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Lakehouse } from '../core/contract.js';
|
|
2
|
+
import { type MessageContext, type SqlQueryInput } from '../core/messages.js';
|
|
3
|
+
/** Authorize every SQL request and supply a backend that independently enforces access and query limits. */
|
|
4
|
+
export declare function createSqlQueryHandler(authorize: (query: SqlQueryInput, context: MessageContext) => Promise<Lakehouse>): (input: SqlQueryInput, context: MessageContext) => Promise<import("../core/contract.js").QueryResult>;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
|
-
import { type ComponentRef } from 'react';
|
|
2
|
-
import { type
|
|
3
|
-
import type {
|
|
4
|
-
|
|
1
|
+
import { type ComponentRef, type ComponentPropsWithoutRef } from 'react';
|
|
2
|
+
import { type DataAppBridgeOptions } from '../../embed/bridge.js';
|
|
3
|
+
import type { DataAppConnection } from '../../embed/host.js';
|
|
4
|
+
import type { DataAppSource } from '../../embed/source.js';
|
|
5
|
+
export type DataAppBridgeProps = Pick<DataAppBridgeOptions, 'onMessage' | 'onStatusChange' | 'onDiagnostic' | 'presentation'> & ({
|
|
6
|
+
source: DataAppSource;
|
|
7
|
+
title: string;
|
|
8
|
+
startupTimeoutMs?: number;
|
|
9
|
+
iframeProps?: Omit<ComponentPropsWithoutRef<'iframe'>, 'src' | 'srcDoc' | 'sandbox' | 'referrerPolicy' | 'loading' | 'children' | 'title'>;
|
|
10
|
+
iframe?: never;
|
|
11
|
+
connection?: never;
|
|
12
|
+
} | {
|
|
5
13
|
iframe: ComponentRef<'iframe'> | null;
|
|
6
14
|
connection: DataAppConnection;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
source?: never;
|
|
16
|
+
title?: never;
|
|
17
|
+
startupTimeoutMs?: never;
|
|
18
|
+
iframeProps?: never;
|
|
19
|
+
});
|
|
20
|
+
/** Owns iframe setup and delivery. The consuming shell owns loading, error UI, and retries. */
|
|
21
|
+
export declare function DataAppBridge(props: DataAppBridgeProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type DataAppSkeletonProps = Omit<ComponentPropsWithRef<'output'>, 'children' | 'role' | 'aria-busy'> & {
|
|
4
|
+
header?: ReactNode;
|
|
5
|
+
footer?: ReactNode;
|
|
6
|
+
};
|
|
7
|
+
/** Host-side placeholder while a data app starts. Header and footer slots remain host-owned; widget placeholders are decorative. */
|
|
8
|
+
export declare function DataAppSkeleton({ header, footer, className, ...props }: DataAppSkeletonProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef, ReactNode } from 'react';
|
|
2
2
|
|
|
3
3
|
/** One search control surface for picker search and table/page search. */
|
|
4
|
-
export declare function SearchInput({ size, endAction, loading, className, ...props }: Omit<ComponentPropsWithRef<'input'>, 'size' | 'children'> & {
|
|
4
|
+
export declare function SearchInput({ size, endAction, loading, className, onKeyDown, ...props }: Omit<ComponentPropsWithRef<'input'>, 'size' | 'children'> & {
|
|
5
5
|
size?: 'default' | 'compact';
|
|
6
6
|
endAction?: ReactNode;
|
|
7
7
|
/** Replaces the search glyph in its existing space; input and results remain usable. */
|
|
@@ -56,6 +56,8 @@ export { EmptyState } from './EmptyState.js';
|
|
|
56
56
|
export type { EmptyStateProps } from './EmptyState.js';
|
|
57
57
|
export { Skeleton } from './Skeleton.js';
|
|
58
58
|
export type { SkeletonProps } from './Skeleton.js';
|
|
59
|
+
export { DataAppSkeleton } from './DataAppSkeleton.js';
|
|
60
|
+
export type { DataAppSkeletonProps } from './DataAppSkeleton.js';
|
|
59
61
|
export { DataBoundary } from './DataBoundary.js';
|
|
60
62
|
export { resolveDataView } from '../../core/data-view.js';
|
|
61
63
|
export { displayedSnapshot } from '../../core/data-view.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type Theme } from '../../core/appearance.js';
|
|
2
|
+
/** The parent theme takes precedence; only standalone viewers receive controls. */
|
|
3
|
+
export declare function useAppAppearance(appearance: unknown, hostTheme?: Theme): import("../../core/appearance.js").ThemeController | undefined;
|
|
@@ -2,7 +2,7 @@ import type { ReactNode } from 'react';
|
|
|
2
2
|
import { type AppVariableValues, type VariableCollection } from './ui/variables.js';
|
|
3
3
|
import type { ResolvedVariables } from './view.js';
|
|
4
4
|
/** URL values resolve before facets; facet keys include their dependent inputs for cached, bounded loading. */
|
|
5
|
-
export declare function useViewVariables<Variables extends VariableCollection>(definitions: Variables, loadFacet: (operation: string, input: unknown, signal: AbortSignal) => Promise<unknown>): {
|
|
5
|
+
export declare function useViewVariables<Variables extends VariableCollection>(definitions: Variables, clientScope: string, loadFacet: (operation: string, input: unknown, signal: AbortSignal) => Promise<unknown>): {
|
|
6
6
|
values: AppVariableValues<Variables>;
|
|
7
7
|
set: <Key extends keyof Variables & string>(name: Key, value: AppVariableValues<Variables>[Key]) => void;
|
|
8
8
|
reset: <Key extends keyof Variables & string>(name: Key) => void;
|
|
@@ -1,4 +1,53 @@
|
|
|
1
|
-
|
|
1
|
+
// src/worker/trusted-parent.ts
|
|
2
|
+
function exactOrigin(value) {
|
|
3
|
+
const url = new URL(value);
|
|
4
|
+
if (!/^https?:$/.test(url.protocol) || url.origin !== value) {
|
|
5
|
+
throw new Error("Expected an exact HTTP(S) parent origin.");
|
|
6
|
+
}
|
|
7
|
+
return url;
|
|
8
|
+
}
|
|
9
|
+
function trustedParent(config, searchParams) {
|
|
10
|
+
if (typeof config !== "string" || !config.trim()) {
|
|
11
|
+
throw new Error("Missing trusted parent origins.");
|
|
12
|
+
}
|
|
13
|
+
const allowed = config.trim().split(/\s+/).map((value) => {
|
|
14
|
+
const wildcard = value.startsWith("https://*.");
|
|
15
|
+
const url = exactOrigin(wildcard ? value.replace("*.", "") : value);
|
|
16
|
+
if (url.hostname.includes("*") || wildcard && url.port) {
|
|
17
|
+
throw new Error("Invalid parent origin pattern.");
|
|
18
|
+
}
|
|
19
|
+
return { value, url, wildcard };
|
|
20
|
+
});
|
|
21
|
+
const requested = searchParams.getAll("__altertable_parent");
|
|
22
|
+
if (!requested.length) {
|
|
23
|
+
const fallback = allowed.find((origin) => !origin.wildcard);
|
|
24
|
+
if (!fallback)
|
|
25
|
+
throw new Error("An exact default parent origin is required.");
|
|
26
|
+
return fallback.value;
|
|
27
|
+
}
|
|
28
|
+
if (requested.length !== 1)
|
|
29
|
+
return null;
|
|
30
|
+
let url;
|
|
31
|
+
try {
|
|
32
|
+
url = exactOrigin(requested[0]);
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const permitted = allowed.some((origin) => {
|
|
37
|
+
if (!origin.wildcard)
|
|
38
|
+
return origin.value === requested[0];
|
|
39
|
+
if (url.protocol !== "https:" || url.port)
|
|
40
|
+
return false;
|
|
41
|
+
const suffix = `.${origin.url.hostname}`;
|
|
42
|
+
return url.hostname.endsWith(suffix) && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(url.hostname.slice(0, -suffix.length));
|
|
43
|
+
});
|
|
44
|
+
return permitted ? requested[0] : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// src/worker/index.ts
|
|
48
|
+
var TOKEN_RE = /^(?=.{1,63}$)[a-z0-9]+(?:-[a-z0-9]+)+-app-[1-9][0-9]*$/;
|
|
49
|
+
var PAGE_CSP = "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; img-src data: blob:; connect-src 'none'; form-action 'none'; base-uri 'none'";
|
|
50
|
+
var inlineBootstrap = `(() => {
|
|
2
51
|
// src/core/bridge.ts
|
|
3
52
|
var BRIDGE = "altertable:data-app";
|
|
4
53
|
var MAX_PENDING = 128;
|
|
@@ -30,6 +79,9 @@
|
|
|
30
79
|
this.name = "MessageRoutingError";
|
|
31
80
|
}
|
|
32
81
|
}
|
|
82
|
+
function defineMessageRoute(route) {
|
|
83
|
+
return route;
|
|
84
|
+
}
|
|
33
85
|
function defineDataQueryRoute(operations) {
|
|
34
86
|
return {
|
|
35
87
|
operations,
|
|
@@ -51,7 +103,7 @@
|
|
|
51
103
|
if (!value || typeof value !== "object")
|
|
52
104
|
throw new Error("Invalid data response.");
|
|
53
105
|
const response = value;
|
|
54
|
-
if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !response.body || typeof response.body !== "object")
|
|
106
|
+
if (!Number.isInteger(response.status) || response.status < 100 || response.status > 599 || !response.body || typeof response.body !== "object" || Array.isArray(response.body))
|
|
55
107
|
throw new Error("Invalid data response.");
|
|
56
108
|
const body = response.body;
|
|
57
109
|
if (response.status < 200 || response.status >= 300) {
|
|
@@ -73,6 +125,28 @@
|
|
|
73
125
|
}
|
|
74
126
|
};
|
|
75
127
|
}
|
|
128
|
+
var sqlQueryRoute = defineMessageRoute({
|
|
129
|
+
input(value) {
|
|
130
|
+
if (!value || typeof value !== "object")
|
|
131
|
+
throw new Error("Invalid SQL query.");
|
|
132
|
+
const query = value;
|
|
133
|
+
if (typeof query.statement !== "string" || !query.statement.trim() || typeof query.limit !== "number" || !Number.isSafeInteger(query.limit) || query.limit < 1)
|
|
134
|
+
throw new Error("Invalid SQL query.");
|
|
135
|
+
return { statement: query.statement, limit: query.limit };
|
|
136
|
+
},
|
|
137
|
+
output(value, input) {
|
|
138
|
+
if (!value || typeof value !== "object")
|
|
139
|
+
throw new Error("Invalid query result.");
|
|
140
|
+
const result = value;
|
|
141
|
+
if (!Array.isArray(result.columns) || !result.columns.every((column) => column && typeof column.name === "string" && (column.type === undefined || typeof column.type === "string")) || !Array.isArray(result.rows) || result.rows.length > input.limit || !result.rows.every((row) => Array.isArray(row) && row.length === result.columns.length) || result.queryId !== undefined && typeof result.queryId !== "string")
|
|
142
|
+
throw new Error("Invalid query result.");
|
|
143
|
+
return {
|
|
144
|
+
columns: result.columns,
|
|
145
|
+
rows: result.rows,
|
|
146
|
+
...result.queryId === undefined ? {} : { queryId: result.queryId }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
});
|
|
76
150
|
|
|
77
151
|
// src/client/messages.ts
|
|
78
152
|
function createMessageClient(routes, transport) {
|
|
@@ -120,6 +194,11 @@
|
|
|
120
194
|
}
|
|
121
195
|
|
|
122
196
|
// src/client/iframe.ts
|
|
197
|
+
function rethrowDataMessageError(error) {
|
|
198
|
+
if (error instanceof MessageRoutingError)
|
|
199
|
+
throw new DataAppError(error.message, error.code, error.requestId);
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
123
202
|
function createIframeTransport({
|
|
124
203
|
parentOrigin,
|
|
125
204
|
window: frame = window,
|
|
@@ -259,15 +338,9 @@
|
|
|
259
338
|
send({ type: "bridge:ready" });
|
|
260
339
|
});
|
|
261
340
|
}
|
|
262
|
-
const messages = createMessageClient({ "data:query": defineDataQueryRoute() }, requestMessage);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return await messages.request("data:query", { operation, input }, { signal });
|
|
266
|
-
} catch (error) {
|
|
267
|
-
if (error instanceof MessageRoutingError)
|
|
268
|
-
throw new DataAppError(error.message, error.code, error.requestId);
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
341
|
+
const messages = createMessageClient({ "data:query": defineDataQueryRoute(), "data:sql": sqlQueryRoute }, requestMessage);
|
|
342
|
+
function queryOperation(operation, input, signal) {
|
|
343
|
+
return messages.request("data:query", { operation, input }, { signal }).catch(rethrowDataMessageError);
|
|
271
344
|
}
|
|
272
345
|
function disconnect() {
|
|
273
346
|
send({ type: "bridge:disconnect" });
|
|
@@ -295,7 +368,12 @@
|
|
|
295
368
|
send({ type: "bridge:ready" });
|
|
296
369
|
return {
|
|
297
370
|
request: requestMessage,
|
|
298
|
-
transport:
|
|
371
|
+
transport: queryOperation,
|
|
372
|
+
lakehouse: {
|
|
373
|
+
queryAll(statement, { limit, signal }) {
|
|
374
|
+
return messages.request("data:sql", { statement, limit }, { signal }).catch(rethrowDataMessageError);
|
|
375
|
+
}
|
|
376
|
+
},
|
|
299
377
|
dispose,
|
|
300
378
|
mode,
|
|
301
379
|
snapshot() {
|
|
@@ -386,3 +464,57 @@
|
|
|
386
464
|
throw new Error("Missing trusted parent origin.");
|
|
387
465
|
startDataAppBootstrap({ parentOrigin });
|
|
388
466
|
})();
|
|
467
|
+
`.replace(/<\/script/gi, (match) => `<\\${match.slice(1)}`);
|
|
468
|
+
function runtimeHtml(parentOrigin) {
|
|
469
|
+
const attribute = parentOrigin.replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">").replaceAll("'", "'");
|
|
470
|
+
return `<!doctype html>
|
|
471
|
+
<html>
|
|
472
|
+
<head>
|
|
473
|
+
<meta charset="utf-8">
|
|
474
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
475
|
+
<meta http-equiv="Content-Security-Policy" content="${PAGE_CSP}">
|
|
476
|
+
</head>
|
|
477
|
+
<body>
|
|
478
|
+
<div id="root"></div>
|
|
479
|
+
<script data-parent-origin="${attribute}">${inlineBootstrap}</script>
|
|
480
|
+
</body>
|
|
481
|
+
</html>
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
484
|
+
function isPreviewHost(hostname, domainName) {
|
|
485
|
+
const suffix = `.${domainName}`;
|
|
486
|
+
return hostname.endsWith(suffix) && TOKEN_RE.test(hostname.slice(0, -suffix.length));
|
|
487
|
+
}
|
|
488
|
+
var worker_default = {
|
|
489
|
+
fetch(request, env) {
|
|
490
|
+
const url = new URL(request.url);
|
|
491
|
+
if (!isPreviewHost(url.hostname, env.DOMAIN_NAME) || url.pathname !== "/") {
|
|
492
|
+
return new Response(null, { status: 404 });
|
|
493
|
+
}
|
|
494
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
495
|
+
return new Response(null, {
|
|
496
|
+
status: 405,
|
|
497
|
+
headers: { Allow: "GET, HEAD" }
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
let parent;
|
|
501
|
+
try {
|
|
502
|
+
parent = trustedParent(env.PARENT_ORIGINS, url.searchParams);
|
|
503
|
+
} catch {
|
|
504
|
+
return new Response(null, { status: 503 });
|
|
505
|
+
}
|
|
506
|
+
if (!parent)
|
|
507
|
+
return new Response(null, { status: 403 });
|
|
508
|
+
return new Response(request.method === "HEAD" ? null : runtimeHtml(parent), {
|
|
509
|
+
headers: {
|
|
510
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
511
|
+
"Content-Security-Policy": `${PAGE_CSP}; frame-ancestors ${env.PARENT_ORIGINS.trim().split(/\s+/).join(" ")}`,
|
|
512
|
+
"X-Content-Type-Options": "nosniff",
|
|
513
|
+
"Referrer-Policy": "no-referrer"
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
export {
|
|
519
|
+
worker_default as default
|
|
520
|
+
};
|
package/docs/app-authoring.md
CHANGED
|
@@ -7,12 +7,12 @@ calls, and reusable React UI.
|
|
|
7
7
|
1. Inspect the source data, time coverage, and existing definitions before choosing
|
|
8
8
|
the exploration. Build findings from observed results and distinguish
|
|
9
9
|
association from cause.
|
|
10
|
-
2. Define named, bounded [operations](contract.md)
|
|
10
|
+
2. Define named, bounded [operations](contract.md) in the execution runtime. Put shared input
|
|
11
11
|
contracts in a browser-safe module and validate outputs before returning them.
|
|
12
|
-
3.
|
|
12
|
+
3. For HTTP apps, use a [server handler](server.md) that authorizes each request, or the
|
|
13
13
|
[Bun adapter](server-bun.md) for local development.
|
|
14
14
|
4. Create a typed [client](client.md) and compose [React views](react.md). Import
|
|
15
|
-
the operation registry with `import type`
|
|
15
|
+
the operation registry with `import type` for HTTP apps, or as a value for bundle apps.
|
|
16
16
|
5. Define glossary and query evidence, handle empty results, and preserve the
|
|
17
17
|
displayed input while a request refreshes or fails. A measured zero and an
|
|
18
18
|
unavailable value must remain distinct.
|
|
@@ -21,6 +21,26 @@ calls, and reusable React UI.
|
|
|
21
21
|
|
|
22
22
|
Import through `@altertable/data-app/<entry>`. Installed package files are
|
|
23
23
|
dependencies; customize the app's own source rather than editing `node_modules`.
|
|
24
|
-
SQL, credentials, and viewer authorization belong on the server.
|
|
24
|
+
For HTTP apps, SQL, credentials, and viewer authorization belong on the server.
|
|
25
|
+
For bundle apps, define operations in the browser and pass them to
|
|
26
|
+
`createDataClient({ operations })`; SQL travels through the authorized
|
|
27
|
+
[SQL bridge](embed.md#sql-query-route). Credentials, viewer authorization, and
|
|
28
|
+
enforced query limits remain backend-owned.
|
|
29
|
+
|
|
30
|
+
## Execution ownership
|
|
31
|
+
|
|
32
|
+
The client selects one of two paths when it is created:
|
|
33
|
+
|
|
34
|
+
| App model | Operation execution | Query delivery |
|
|
35
|
+
| ---------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
36
|
+
| HTTP app | The browser sends a name and input; the server authorizes the request and runs its registered operation. | A server-owned lakehouse adapter executes SQL. |
|
|
37
|
+
| Bundle app | The browser directly runs the registry passed to `createDataClient({ operations })`. | `bridge.lakehouse` sends SQL to the host, which authorizes each query and supplies a backend adapter. |
|
|
38
|
+
|
|
39
|
+
Both paths use the same internal operation executor for input/output validation,
|
|
40
|
+
query names, row bounds, deadlines, evidence, and response serialization limits.
|
|
41
|
+
The executor accepts a `Lakehouse` and never chooses a transport or authorizes a
|
|
42
|
+
viewer. Each adapter owns its boundary: HTTP owns request parsing and responses;
|
|
43
|
+
the bridge owns message delivery; the SQL host owns authorization and public query
|
|
44
|
+
errors. Backend permissions and resource limits apply independently of app policy.
|
|
25
45
|
|
|
26
46
|
For agent-assisted authoring, see the [starter AGENTS.md template](starter-agent-instructions.md).
|
package/docs/appearance.md
CHANGED
|
@@ -1,30 +1,43 @@
|
|
|
1
1
|
# Appearance
|
|
2
2
|
|
|
3
3
|
Import `parseAppearance`, `applyAppearance`, and `createThemeController` from
|
|
4
|
-
`@altertable/data-app/appearance`. Parsing is safe on the server
|
|
5
|
-
|
|
4
|
+
`@altertable/data-app/appearance`. Parsing is safe on the server. Applying tokens requires a browser document;
|
|
5
|
+
viewer preferences use browser storage.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
import { parseAppearance } from '@altertable/data-app/appearance';
|
|
9
9
|
|
|
10
10
|
const appearance = parseAppearance({
|
|
11
|
-
|
|
11
|
+
theme: 'system',
|
|
12
12
|
accentColor: '#405d47',
|
|
13
13
|
density: 'comfortable',
|
|
14
14
|
});
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
`parseAppearance` fills omitted settings with defaults and rejects unknown keys
|
|
18
|
-
or invalid values. Settings include light/dark/system
|
|
18
|
+
or invalid values. Settings include light/dark/system theme, neutral/slate/warm
|
|
19
19
|
base colors, accent colors, a chart palette, density, corner radius, elevation,
|
|
20
20
|
and body/heading typography. Colors are six-digit hexadecimal values.
|
|
21
21
|
|
|
22
22
|
`applyAppearance(appearance)` installs semantic CSS tokens on the document root
|
|
23
23
|
and returns a cleanup function for system-theme listening.
|
|
24
|
-
`createThemeController(
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
the
|
|
24
|
+
`createThemeController(initialTheme)` manages the viewer's preference through
|
|
25
|
+
`getTheme`, `setTheme`, and `subscribe`. It reads and persists local storage
|
|
26
|
+
when available. Apply the preference with `applyAppearance` in the UI owner;
|
|
27
|
+
the controller itself does not modify the document.
|
|
28
28
|
|
|
29
29
|
The React `DataApp` shell manages appearance for normal app usage. See
|
|
30
30
|
[configuration](config.md), [React](react.md), and [styles](react-styles.md).
|
|
31
|
+
|
|
32
|
+
`Theme` is the resolved `'light' | 'dark'` theme. `ThemePreference` additionally
|
|
33
|
+
allows `'system'` for standalone viewers. A trusted host supplies a `theme: Theme`
|
|
34
|
+
through [parent presentation](embed.md#parent-presentation). React applies that
|
|
35
|
+
theme directly with app-owned brand tokens; standalone viewers use the preference
|
|
36
|
+
controller. Appearance effect cleanup releases the system preference listener.
|
|
37
|
+
|
|
38
|
+
When migrating theme controls, replace `getMode`/`setMode` with
|
|
39
|
+
`getTheme`/`setTheme`, and initialize the controller with a `ThemePreference`
|
|
40
|
+
rather than appearance settings. Subscribe to the controller and compose its
|
|
41
|
+
preference with `applyAppearance` to update document tokens.
|
|
42
|
+
|
|
43
|
+
Appearance configuration uses `theme` (formerly `mode`).
|