@altertable/data-app 0.61.0 → 0.63.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 +4 -32
- package/CONTRIBUTING.md +63 -72
- package/README.md +14 -38
- package/dist/chunks/{contract-d9skd1n7.js → contract-8wdybxj7.js} +163 -16
- package/dist/chunks/contract-8wdybxj7.js.map +13 -0
- package/dist/chunks/{contract-ryyf6dme.js → contract-cxr9t12b.js} +2 -5
- package/dist/chunks/contract-cxr9t12b.js.map +10 -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-mev09s5v.js.map +2 -2
- package/dist/chunks/{contract-ehk50k7e.js → contract-tf8c3qpv.js} +34 -4
- package/dist/chunks/contract-tf8c3qpv.js.map +11 -0
- package/dist/chunks/{contract-zr4s2g7m.js → contract-tkc552ze.js} +75 -36
- package/dist/chunks/contract-tkc552ze.js.map +12 -0
- package/dist/chunks/{contract-nt819swq.js → contract-wd8qe3mt.js} +6 -1
- package/dist/chunks/{contract-nt819swq.js.map → contract-wd8qe3mt.js.map} +3 -3
- package/dist/chunks/contract-wz59z8pq.js.map +1 -1
- package/dist/chunks/{contract-7gpsee0v.js → contract-zr3jd7mr.js} +51 -22
- package/dist/chunks/contract-zr3jd7mr.js.map +12 -0
- package/dist/client/index.js +7 -6
- package/dist/client/index.js.map +1 -1
- package/dist/core/appearance.js +1 -1
- package/dist/core/contract.js +6 -4
- package/dist/core/contract.js.map +1 -1
- package/dist/embed/index.js +41 -9
- package/dist/embed/index.js.map +5 -4
- package/dist/local.js +172 -84
- package/dist/local.js.map +7 -8
- package/dist/react/embed/index.js +83 -99
- package/dist/react/embed/index.js.map +4 -5
- package/dist/react/index.js +5701 -324
- package/dist/react/index.js.map +74 -65
- package/dist/server.js +153 -81
- package/dist/server.js.map +6 -7
- package/dist/types/client/data-client.d.ts +33 -0
- package/dist/types/client/iframe.d.ts +14 -0
- package/dist/types/client/index.d.ts +4 -34
- package/dist/types/client/location.d.ts +4 -6
- package/dist/types/client/navigation.d.ts +2 -1
- package/dist/types/core/appearance.d.ts +7 -6
- package/dist/types/core/bridge.d.ts +2 -8
- package/dist/types/core/config.d.ts +1 -1
- package/dist/types/core/contract.d.ts +9 -56
- package/dist/types/core/format.d.ts +0 -1
- package/dist/types/core/messages.d.ts +11 -30
- package/dist/types/core/navigation.d.ts +11 -0
- package/dist/types/core/operation-types.d.ts +76 -0
- package/dist/types/core/operation.d.ts +21 -0
- package/dist/types/core/presentation.d.ts +7 -0
- package/dist/types/core/variables.d.ts +2 -2
- package/dist/types/embed/bridge.d.ts +12 -0
- package/dist/types/embed/host.d.ts +14 -5
- package/dist/types/embed/index.d.ts +6 -4
- package/dist/types/embed/source.d.ts +15 -0
- package/dist/types/embed/sql.d.ts +4 -0
- package/dist/types/react/content.d.ts +3 -3
- package/dist/types/react/embed/bridge.d.ts +19 -10
- package/dist/types/react/embed/index.d.ts +0 -2
- package/dist/types/react/hooks.d.ts +65 -64
- package/dist/types/react/index.d.ts +135 -2
- package/dist/types/react/injectStyles.d.ts +7 -0
- package/dist/types/react/shellStyles.d.ts +3 -0
- package/dist/types/react/styles.d.ts +8 -0
- package/dist/types/react/ui/AboutData.d.ts +0 -1
- package/dist/types/react/ui/AppFooter.d.ts +0 -1
- package/dist/types/react/ui/AppHeader.d.ts +0 -1
- package/dist/types/react/ui/AppLayout.d.ts +0 -1
- package/dist/types/react/ui/AppScope.d.ts +0 -1
- package/dist/types/react/ui/AppToolbar.d.ts +0 -1
- package/dist/types/react/ui/Breakdown.d.ts +0 -1
- package/dist/types/react/ui/Button.d.ts +0 -1
- package/dist/types/react/ui/Checkbox.d.ts +0 -1
- package/dist/types/react/ui/Combobox.d.ts +0 -1
- package/dist/types/react/ui/ComparisonVisual.d.ts +0 -1
- package/dist/types/react/ui/ContentSkeleton.d.ts +2 -5
- package/dist/types/react/ui/DataApp.d.ts +2 -2
- package/dist/types/react/ui/DataAppSkeleton.d.ts +7 -0
- package/dist/types/react/ui/DataBoundary.d.ts +0 -1
- package/dist/types/react/ui/DataSection.d.ts +2 -4
- package/dist/types/react/ui/DataTable.d.ts +2 -3
- package/dist/types/react/ui/DataViewToast.d.ts +0 -1
- package/dist/types/react/ui/DataWidget.d.ts +4 -14
- package/dist/types/react/ui/DateRangePicker.d.ts +0 -1
- package/dist/types/react/ui/DateTimeTooltip.d.ts +0 -1
- package/dist/types/react/ui/EmptyState.d.ts +2 -5
- package/dist/types/react/ui/GettingStarted.d.ts +0 -1
- package/dist/types/react/ui/GlossaryDefinition.d.ts +0 -1
- package/dist/types/react/ui/GlossaryExplanation.d.ts +0 -1
- package/dist/types/react/ui/GradientScroll.d.ts +0 -1
- package/dist/types/react/ui/Grid.d.ts +0 -1
- package/dist/types/react/ui/HelpPopover.d.ts +0 -2
- package/dist/types/react/ui/Kbd.d.ts +0 -1
- package/dist/types/react/ui/LiveControl.d.ts +0 -1
- package/dist/types/react/ui/MetricWidget.d.ts +0 -2
- package/dist/types/react/ui/PeriodSummary.d.ts +0 -1
- package/dist/types/react/ui/PresentStory.d.ts +0 -1
- package/dist/types/react/ui/QueryList.d.ts +0 -1
- package/dist/types/react/ui/Ranking.d.ts +0 -1
- package/dist/types/react/ui/RefreshControl.d.ts +0 -1
- package/dist/types/react/ui/RefreshRegion.d.ts +0 -1
- package/dist/types/react/ui/RequestHint.d.ts +0 -1
- package/dist/types/react/ui/SearchField.d.ts +0 -1
- package/dist/types/react/ui/SearchInput.d.ts +1 -2
- package/dist/types/react/ui/SearchMatch.d.ts +0 -1
- package/dist/types/react/ui/SelectableBarChart.d.ts +0 -1
- package/dist/types/react/ui/SelectionMark.d.ts +0 -1
- package/dist/types/react/ui/Sheet.d.ts +0 -1
- package/dist/types/react/ui/Skeleton.d.ts +0 -1
- package/dist/types/react/ui/Stack.d.ts +0 -1
- package/dist/types/react/ui/StatusPanel.d.ts +0 -1
- package/dist/types/react/ui/TableWidget.d.ts +2 -3
- package/dist/types/react/ui/Tabs.d.ts +0 -1
- package/dist/types/react/ui/TextContent.d.ts +4 -0
- package/dist/types/react/ui/TextWidget.d.ts +19 -0
- package/dist/types/react/ui/ThemeSelector.d.ts +0 -1
- package/dist/types/react/ui/Tooltip.d.ts +0 -1
- package/dist/types/react/ui/UpdatedAt.d.ts +0 -1
- package/dist/types/react/ui/VariableBar.d.ts +0 -1
- package/dist/types/react/ui/VisualizationWidget.d.ts +5 -13
- package/dist/types/react/ui/WidgetDisclosure.d.ts +0 -1
- package/dist/types/react/ui/WidgetViewTabs.d.ts +2 -3
- package/dist/types/react/ui/data-identifiers.d.ts +0 -1
- package/dist/types/react/ui/presentation.d.ts +19 -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 +2 -2
- package/dist/types/react/view.d.ts +2 -2
- package/dist/{bootstrap.js → worker.js} +144 -12
- package/docs/app-authoring.md +60 -24
- package/docs/client.md +48 -19
- package/docs/contract.md +10 -10
- package/docs/embed.md +94 -26
- package/docs/hosted-apps.md +39 -0
- package/docs/local-data-apps.md +14 -0
- package/docs/react-embed.md +75 -24
- package/docs/react.md +118 -93
- package/docs/server-bun.md +4 -1
- package/docs/server.md +2 -2
- package/docs/worker.md +55 -0
- package/examples/starter-data-app/index.tsx +159 -0
- package/package.json +18 -12
- 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-ryyf6dme.js.map +0 -10
- package/dist/chunks/contract-zr4s2g7m.js.map +0 -12
- package/dist/react/index.css +0 -4478
- package/dist/react.css.d.ts +0 -1
- package/dist/types/embed/shell.d.ts +0 -21
- package/dist/types/embed/standalone.d.ts +0 -1
- package/dist/types/react/embed/shell.d.ts +0 -10
- package/dist/types/react/ui/index.d.ts +0 -129
- package/docs/appearance.md +0 -30
- package/docs/bootstrap.md +0 -76
- package/docs/config.md +0 -21
- package/docs/format.md +0 -29
- package/docs/react-styles.md +0 -17
- package/docs/starter-agent-instructions.md +0 -53
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EmptyContent } from './ui/presentation.js';
|
|
2
2
|
import type { AppVariableValues, DateRangeVariable, VariableCollection } from '../core/variables.js';
|
|
3
3
|
import type { DateRangeRequest } from '../core/contract.js';
|
|
4
4
|
export type ResolvedVariables<Variables extends VariableCollection> = {
|
|
@@ -23,7 +23,7 @@ export type DataViewDefinition<Name extends string, Variables extends VariableCo
|
|
|
23
23
|
bindings?: ViewBindings<Variables, Input>;
|
|
24
24
|
/** App-owned semantics: measured zero need not mean an empty result. */
|
|
25
25
|
isEmpty: (data: Data) => boolean;
|
|
26
|
-
empty:
|
|
26
|
+
empty: EmptyContent;
|
|
27
27
|
} & ({
|
|
28
28
|
date: ViewDate<Variables, Input>;
|
|
29
29
|
describeInput?: (input: Input) => string;
|
|
@@ -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
|
@@ -1,26 +1,62 @@
|
|
|
1
1
|
# Author a data app
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
3
|
+
Build an exploration that answers the user's question and a story that presents
|
|
4
|
+
its strongest findings. Both use the same queries, definitions, and evidence.
|
|
5
|
+
|
|
6
|
+
## Inspect the data
|
|
7
|
+
|
|
8
|
+
Inspect the relevant catalogs, tables, and fields, their time coverage, and
|
|
9
|
+
existing definitions. Choose a question the available data can answer.
|
|
10
|
+
|
|
11
|
+
## Build the exploration
|
|
12
|
+
|
|
13
|
+
Choose the execution path:
|
|
14
|
+
|
|
15
|
+
| App | Guide |
|
|
16
|
+
| ---------------------------------- | --------------------------------------- |
|
|
17
|
+
| Data app (hosted / remote / cloud) | [Single-file authoring](hosted-apps.md) |
|
|
18
|
+
| Local data app | [Local authoring](local-data-apps.md) |
|
|
19
|
+
|
|
20
|
+
Lead with a supported finding and expose the relevant fields as filter variables. Use a date filter for questions worth exploring over time,
|
|
21
|
+
or a fixed period snapshot for a deliberate historical analysis.
|
|
22
|
+
Register inspected tables and fields with `defineDataIdentifiers()` and use
|
|
23
|
+
`<DataIdentifier>` when naming sources. Register terms and query evidence so
|
|
24
|
+
readers can inspect the source of each claim.
|
|
25
|
+
|
|
26
|
+
Connect visualizations with introductions and explanations. Use `<TextWidget>`
|
|
27
|
+
for a narrative panel with the standard widget frame, or `<TextContent>` for
|
|
28
|
+
borderless prose. Bind claims to `result.select((data, input) => ...)` so their
|
|
29
|
+
values and scope follow the displayed results through filter changes, refresh,
|
|
30
|
+
and failure.
|
|
31
|
+
|
|
32
|
+
| Task | Documentation |
|
|
33
|
+
| ------------------------------------------ | ---------------------------------------------------------- |
|
|
34
|
+
| Define queries, inputs, and result parsing | [Operations](contract.md) |
|
|
35
|
+
| Build views, filters, and request states | [React](react.md) |
|
|
36
|
+
| Introduce and explain visualizations | [Narrative text](react.md#narrative-text) |
|
|
37
|
+
| Find formatters and presentation helpers | [App helpers](react.md#reuse-app-helpers) |
|
|
38
|
+
| Register source names | [Source identifiers](react.md#register-source-identifiers) |
|
|
39
|
+
| Choose date and field filters | [Filter variables](react.md#time-views-and-field-filters) |
|
|
40
|
+
| Handle refresh and stale results | [Displayed results](react.md#preserve-displayed-results) |
|
|
41
|
+
| Bind definitions and source evidence | [Data context](react.md#bind-evidence) |
|
|
42
|
+
|
|
43
|
+
Use the exported types for configuration, appearance, formatting, and component
|
|
44
|
+
options.
|
|
45
|
+
|
|
46
|
+
## Present the findings
|
|
47
|
+
|
|
48
|
+
Compose a [story](react.md#present-data-with-stories) from the exploration's
|
|
49
|
+
findings. Lead with the answer, then show the evidence and comparisons that
|
|
50
|
+
explain it. Select the findings that matter to the audience; do not turn every
|
|
51
|
+
row or chart into a step.
|
|
52
|
+
|
|
53
|
+
## Verify the app
|
|
54
|
+
|
|
55
|
+
Verify findings against the source and the user's question. Distinguish measured
|
|
56
|
+
zero, unavailable values, and empty results. Check filters, refresh, loading,
|
|
57
|
+
empty, error, and stale states, then present the story. Inspect both experiences
|
|
58
|
+
at phone and desktop widths in light and dark themes.
|
|
59
|
+
|
|
60
|
+
The app owns its queries, result parsing, business definitions, configuration,
|
|
61
|
+
and presentation. Credentials, authorization, and enforced access/query limits
|
|
62
|
+
stay backend-owned. Edit app-owned files; installed package files are dependencies.
|
package/docs/client.md
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
# Client
|
|
2
2
|
|
|
3
|
-
Import `createDataClient` and `DataAppError` from
|
|
3
|
+
Import `createDataClient()` and `DataAppError` from
|
|
4
4
|
`@altertable/data-app/client`. The client uses Fetch APIs and has no React or
|
|
5
5
|
server dependency.
|
|
6
6
|
|
|
7
|
+
## HTTP operations
|
|
8
|
+
|
|
7
9
|
```ts
|
|
8
10
|
import { createDataClient } from '@altertable/data-app/client';
|
|
9
11
|
import type { operations } from './operations';
|
|
@@ -22,20 +24,58 @@ implementation. Calls POST JSON to `/api/data/:operation`; the client sends
|
|
|
22
24
|
operation inputs rather than SQL or credentials.
|
|
23
25
|
|
|
24
26
|
`DataResponse` contains `data`, the exact request `input`, `requestId`,
|
|
25
|
-
`queriedAt`, and `queryIds`. `queries` is present
|
|
26
|
-
|
|
27
|
+
`queriedAt`, and `queryIds`. `queries` is present when the operation exposes SQL
|
|
28
|
+
and the execution runtime permits disclosure. Use the returned input when labeling stale data during a refresh.
|
|
29
|
+
HTTP and iframe delivery validate the same success envelope: data, request ID,
|
|
30
|
+
query timestamp, query IDs, and optional query evidence. Malformed responses
|
|
31
|
+
reject with `invalid_response`.
|
|
32
|
+
|
|
27
33
|
`DataAppError` exposes a `code` and optional `requestId`; cancellation follows
|
|
28
34
|
the supplied abort signal.
|
|
29
35
|
|
|
30
36
|
See [contracts](contract.md), [server handlers](server.md), and
|
|
31
37
|
[React bindings](react.md).
|
|
32
38
|
|
|
39
|
+
## Browser-owned operations for bundle apps
|
|
40
|
+
|
|
41
|
+
Start with the complete [single-file example](../examples/starter-data-app/index.tsx)
|
|
42
|
+
and [data app authoring guide](hosted-apps.md).
|
|
43
|
+
Bundle apps pass their operation registry as a value:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { connectionCheck } from '@altertable/data-app/contract';
|
|
47
|
+
import { createDataClient } from '@altertable/data-app/client';
|
|
48
|
+
|
|
49
|
+
const client = createDataClient({
|
|
50
|
+
operations: { connection: connectionCheck() },
|
|
51
|
+
});
|
|
52
|
+
const response = await client.query('connection', {});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The client runs input parsing, operation logic, and output parsing in the browser.
|
|
56
|
+
Operation policy bounds rows, duration, and response size and records query evidence. Each query sends `{ statement, limit }`
|
|
57
|
+
to the installed iframe bridge's `data:sql` route; the host needs no operation
|
|
58
|
+
registry. SQL is visible in the browser, even when `exposeSql` is false; that flag
|
|
59
|
+
only controls evidence in the returned response. Credentials remain backend-owned.
|
|
60
|
+
|
|
61
|
+
The trusted bootstrap installs the bridge for bundle apps. A custom runtime must
|
|
62
|
+
install it before querying. An explicit `lakehouse` can supply another authorized
|
|
63
|
+
adapter, including `bridge.lakehouse` or a local server adapter. `operations` cannot
|
|
64
|
+
be combined with `transport`, `endpoint`, or `fetch`; a `lakehouse` requires
|
|
65
|
+
`operations`. The exported `DataClientOptions` union rejects mixed configurations
|
|
66
|
+
at compile time. Omitting `operations` preserves named HTTP/iframe operation delivery.
|
|
67
|
+
|
|
68
|
+
The host must implement and authorize the [SQL route](embed.md#sql-query-route).
|
|
69
|
+
Browser policies improve app behavior; backend access and resource limits must be
|
|
70
|
+
enforced independently because a frame can forge requests. Cancellation reaches
|
|
71
|
+
the host through the existing bridge cancellation protocol.
|
|
72
|
+
|
|
33
73
|
## Iframe transport
|
|
34
74
|
|
|
35
75
|
`createDataClient({ transport })` accepts a `DataTransport`. Without an explicit
|
|
36
76
|
transport, endpoint, or Fetch implementation, it discovers an installed iframe
|
|
37
77
|
transport before falling back to HTTP. Explicit endpoint/Fetch options select
|
|
38
|
-
HTTP. `createHttpTransport` provides the underlying operation delivery adapter.
|
|
78
|
+
HTTP. `createHttpTransport()` provides the underlying operation delivery adapter.
|
|
39
79
|
|
|
40
80
|
A URL-hosted app configures trust and installs the bridge before mounting:
|
|
41
81
|
|
|
@@ -56,8 +96,8 @@ URL controls attach the optional navigation adapter to that connection. Cleanup
|
|
|
56
96
|
the installation and disposes pending work. Bundle apps receive this installation
|
|
57
97
|
from the [trusted bootstrap](embed.md#trusted-bootstrap).
|
|
58
98
|
|
|
59
|
-
`getDataAppTransport()` returns the explicitly installed bridge. Its `request`
|
|
60
|
-
|
|
99
|
+
`getDataAppTransport()` returns the explicitly installed bridge. Its `request()`
|
|
100
|
+
method supports custom typed routes:
|
|
61
101
|
|
|
62
102
|
```ts
|
|
63
103
|
import {
|
|
@@ -92,7 +132,7 @@ navigation.publish('replace');
|
|
|
92
132
|
navigation.dispose();
|
|
93
133
|
```
|
|
94
134
|
|
|
95
|
-
The adapter provides `snapshot`, `subscribe`, and `update`. Opaque sandboxes keep
|
|
135
|
+
The adapter provides `snapshot()`, `subscribe()`, and `update()`. Opaque sandboxes keep
|
|
96
136
|
search/hash in memory; URL frames preserve their URL and local-preview parent
|
|
97
137
|
marker. Host Back/Forward state is applied without publishing it back.
|
|
98
138
|
|
|
@@ -100,14 +140,6 @@ marker. Host Back/Forward state is applied without publishing it back.
|
|
|
100
140
|
and shares one adapter per document. React mounting and URL-backed controls call
|
|
101
141
|
it automatically. Apps that only use data delivery do not attach navigation.
|
|
102
142
|
|
|
103
|
-
Migration: replace `bridge.appLocation` with the navigation adapter and
|
|
104
|
-
`bridge.location(mode)` with `navigation.publish(mode)`. The version-1 wire envelope
|
|
105
|
-
now carries host context in `initialize.state` and subsequent `state` messages;
|
|
106
|
-
upgrade independently deployed hosts and runtimes together.
|
|
107
|
-
Transport state is shared per window across separately bundled entry points;
|
|
108
|
-
install only one transport per document. Public error classes retain `instanceof`
|
|
109
|
-
recognition across independent bootstrap and app bundles in that window.
|
|
110
|
-
|
|
111
143
|
## Pending request limits
|
|
112
144
|
|
|
113
145
|
Each iframe bridge accepts at most 128 unresolved requests, including requests
|
|
@@ -115,10 +147,7 @@ waiting for the connection handshake. Further calls reject with `bridge_busy`
|
|
|
115
147
|
until a pending call completes, is cancelled, or times out. This bounds the
|
|
116
148
|
client's promises, timers, and queued messages.
|
|
117
149
|
|
|
118
|
-
|
|
119
|
-
messages directly without using the client helper. The shared cap is a resource
|
|
120
|
-
policy, not a requirement of the message protocol. It rejects excess requests;
|
|
121
|
-
it does not queue them or limit the total number of calls over a session.
|
|
150
|
+
Hosts enforce their own request limits independently of the client.
|
|
122
151
|
|
|
123
152
|
A failed local-preview verification can be retried by the next explicit data
|
|
124
153
|
request. Concurrent callers share the current verification attempt; aborting
|
package/docs/contract.md
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Import operation definitions, parsers, and shared types from
|
|
4
4
|
`@altertable/data-app/contract`. This entry is safe to import in browser and server
|
|
5
|
-
modules.
|
|
5
|
+
modules. For HTTP apps, keep SQL and operation implementations on the server; browser modules
|
|
6
6
|
should import their operation types using `import type`.
|
|
7
7
|
|
|
8
8
|
## Execute named queries
|
|
9
9
|
|
|
10
|
-
The app supplies `calendar`, `parseActivity`, `checkInput`, `buildActivitySql`,
|
|
11
|
-
and `parseActivityRows` in this example.
|
|
10
|
+
The app supplies `calendar`, `parseActivity()`, `checkInput`, `buildActivitySql()`,
|
|
11
|
+
and `parseActivityRows()` in this example.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
14
|
import {
|
|
@@ -30,14 +30,14 @@ const activity = defineOperation({
|
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
`query` inherits the operation's limit and cancellation signal; `{ limit }` can lower a particular query's bound. Names are checked by TypeScript and at runtime.
|
|
33
|
+
`query()` inherits the operation's limit and cancellation signal; `{ limit }` can lower a particular query's bound. Names are checked by TypeScript and at runtime. Responses include executed SQL and query IDs when disclosure is allowed. HTTP browser modules import operation types with `import type`. Bundle apps import their browser-owned operation registry as a value and use [browser execution](client.md#browser-owned-operations-for-bundle-apps). Never bundle credentials or server adapters.
|
|
34
34
|
|
|
35
35
|
## Shared date ranges
|
|
36
36
|
|
|
37
|
-
Use `defineDateRangeContract` in a browser-safe module to share source coverage,
|
|
37
|
+
Use `defineDateRangeContract()` in a browser-safe module to share source coverage,
|
|
38
38
|
time zone, maximum range, and comparison rules between the server parser and
|
|
39
|
-
React variables. The server uses `calendar.parseRequest`; React uses the same
|
|
40
|
-
contract with `dateRangeVariable`.
|
|
39
|
+
React variables. The server uses `calendar.parseRequest()`; React uses the same
|
|
40
|
+
contract with `dateRangeVariable()`.
|
|
41
41
|
|
|
42
42
|
```ts
|
|
43
43
|
import { defineDateRangeContract } from '@altertable/data-app/contract';
|
|
@@ -49,7 +49,7 @@ export const calendar = defineDateRangeContract({
|
|
|
49
49
|
});
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
`parseEmptyInput`, `parseTrue`, `parseCount`, and `parseDateRangeInput` validate
|
|
52
|
+
`parseEmptyInput()`, `parseTrue()`, `parseCount()`, and `parseDateRangeInput()` validate
|
|
53
53
|
common inputs and results. `connectionCheck()` defines a bounded connectivity
|
|
54
54
|
operation. A successful connectivity check confirms access; it is not an
|
|
55
55
|
analysis result.
|
|
@@ -83,14 +83,14 @@ const router = createMessageRouter(routes, {
|
|
|
83
83
|
});
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
The app defines `parseString` to validate unknown values. `router.dispatch` checks
|
|
86
|
+
The app defines `parseString()` to validate unknown values. `router.dispatch()` checks
|
|
87
87
|
registered routes, input, output, and cancellation. `MessageRoutingError` exposes
|
|
88
88
|
an intentional public code, message, and optional request ID; other handler
|
|
89
89
|
errors are replaced with a generic failure.
|
|
90
90
|
|
|
91
91
|
`defineDataQueryRoute(operationContracts)` validates the selected operation's
|
|
92
92
|
input and output and preserves its response evidence. It infers the result type
|
|
93
|
-
from the selected operation when used with `createMessageClient`. Share input and
|
|
93
|
+
from the selected operation when used with `createMessageClient()`. Share input and
|
|
94
94
|
output parsers, not operation implementations containing SQL or credentials.
|
|
95
95
|
`dataAppRoutes` supplies generic `data:query` and `navigation:update` contracts;
|
|
96
96
|
generic hosts must delegate operation validation and authorization to their
|