@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.
Files changed (159) hide show
  1. package/AGENTS.md +4 -32
  2. package/CONTRIBUTING.md +63 -72
  3. package/README.md +14 -38
  4. package/dist/chunks/{contract-d9skd1n7.js → contract-8wdybxj7.js} +163 -16
  5. package/dist/chunks/contract-8wdybxj7.js.map +13 -0
  6. package/dist/chunks/{contract-ryyf6dme.js → contract-cxr9t12b.js} +2 -5
  7. package/dist/chunks/contract-cxr9t12b.js.map +10 -0
  8. package/dist/chunks/{contract-14vxdcrs.js → contract-farfe948.js} +17 -21
  9. package/dist/chunks/contract-farfe948.js.map +10 -0
  10. package/dist/chunks/contract-mev09s5v.js.map +2 -2
  11. package/dist/chunks/{contract-ehk50k7e.js → contract-tf8c3qpv.js} +34 -4
  12. package/dist/chunks/contract-tf8c3qpv.js.map +11 -0
  13. package/dist/chunks/{contract-zr4s2g7m.js → contract-tkc552ze.js} +75 -36
  14. package/dist/chunks/contract-tkc552ze.js.map +12 -0
  15. package/dist/chunks/{contract-nt819swq.js → contract-wd8qe3mt.js} +6 -1
  16. package/dist/chunks/{contract-nt819swq.js.map → contract-wd8qe3mt.js.map} +3 -3
  17. package/dist/chunks/contract-wz59z8pq.js.map +1 -1
  18. package/dist/chunks/{contract-7gpsee0v.js → contract-zr3jd7mr.js} +51 -22
  19. package/dist/chunks/contract-zr3jd7mr.js.map +12 -0
  20. package/dist/client/index.js +7 -6
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/core/appearance.js +1 -1
  23. package/dist/core/contract.js +6 -4
  24. package/dist/core/contract.js.map +1 -1
  25. package/dist/embed/index.js +41 -9
  26. package/dist/embed/index.js.map +5 -4
  27. package/dist/local.js +172 -84
  28. package/dist/local.js.map +7 -8
  29. package/dist/react/embed/index.js +83 -99
  30. package/dist/react/embed/index.js.map +4 -5
  31. package/dist/react/index.js +5701 -324
  32. package/dist/react/index.js.map +74 -65
  33. package/dist/server.js +153 -81
  34. package/dist/server.js.map +6 -7
  35. package/dist/types/client/data-client.d.ts +33 -0
  36. package/dist/types/client/iframe.d.ts +14 -0
  37. package/dist/types/client/index.d.ts +4 -34
  38. package/dist/types/client/location.d.ts +4 -6
  39. package/dist/types/client/navigation.d.ts +2 -1
  40. package/dist/types/core/appearance.d.ts +7 -6
  41. package/dist/types/core/bridge.d.ts +2 -8
  42. package/dist/types/core/config.d.ts +1 -1
  43. package/dist/types/core/contract.d.ts +9 -56
  44. package/dist/types/core/format.d.ts +0 -1
  45. package/dist/types/core/messages.d.ts +11 -30
  46. package/dist/types/core/navigation.d.ts +11 -0
  47. package/dist/types/core/operation-types.d.ts +76 -0
  48. package/dist/types/core/operation.d.ts +21 -0
  49. package/dist/types/core/presentation.d.ts +7 -0
  50. package/dist/types/core/variables.d.ts +2 -2
  51. package/dist/types/embed/bridge.d.ts +12 -0
  52. package/dist/types/embed/host.d.ts +14 -5
  53. package/dist/types/embed/index.d.ts +6 -4
  54. package/dist/types/embed/source.d.ts +15 -0
  55. package/dist/types/embed/sql.d.ts +4 -0
  56. package/dist/types/react/content.d.ts +3 -3
  57. package/dist/types/react/embed/bridge.d.ts +19 -10
  58. package/dist/types/react/embed/index.d.ts +0 -2
  59. package/dist/types/react/hooks.d.ts +65 -64
  60. package/dist/types/react/index.d.ts +135 -2
  61. package/dist/types/react/injectStyles.d.ts +7 -0
  62. package/dist/types/react/shellStyles.d.ts +3 -0
  63. package/dist/types/react/styles.d.ts +8 -0
  64. package/dist/types/react/ui/AboutData.d.ts +0 -1
  65. package/dist/types/react/ui/AppFooter.d.ts +0 -1
  66. package/dist/types/react/ui/AppHeader.d.ts +0 -1
  67. package/dist/types/react/ui/AppLayout.d.ts +0 -1
  68. package/dist/types/react/ui/AppScope.d.ts +0 -1
  69. package/dist/types/react/ui/AppToolbar.d.ts +0 -1
  70. package/dist/types/react/ui/Breakdown.d.ts +0 -1
  71. package/dist/types/react/ui/Button.d.ts +0 -1
  72. package/dist/types/react/ui/Checkbox.d.ts +0 -1
  73. package/dist/types/react/ui/Combobox.d.ts +0 -1
  74. package/dist/types/react/ui/ComparisonVisual.d.ts +0 -1
  75. package/dist/types/react/ui/ContentSkeleton.d.ts +2 -5
  76. package/dist/types/react/ui/DataApp.d.ts +2 -2
  77. package/dist/types/react/ui/DataAppSkeleton.d.ts +7 -0
  78. package/dist/types/react/ui/DataBoundary.d.ts +0 -1
  79. package/dist/types/react/ui/DataSection.d.ts +2 -4
  80. package/dist/types/react/ui/DataTable.d.ts +2 -3
  81. package/dist/types/react/ui/DataViewToast.d.ts +0 -1
  82. package/dist/types/react/ui/DataWidget.d.ts +4 -14
  83. package/dist/types/react/ui/DateRangePicker.d.ts +0 -1
  84. package/dist/types/react/ui/DateTimeTooltip.d.ts +0 -1
  85. package/dist/types/react/ui/EmptyState.d.ts +2 -5
  86. package/dist/types/react/ui/GettingStarted.d.ts +0 -1
  87. package/dist/types/react/ui/GlossaryDefinition.d.ts +0 -1
  88. package/dist/types/react/ui/GlossaryExplanation.d.ts +0 -1
  89. package/dist/types/react/ui/GradientScroll.d.ts +0 -1
  90. package/dist/types/react/ui/Grid.d.ts +0 -1
  91. package/dist/types/react/ui/HelpPopover.d.ts +0 -2
  92. package/dist/types/react/ui/Kbd.d.ts +0 -1
  93. package/dist/types/react/ui/LiveControl.d.ts +0 -1
  94. package/dist/types/react/ui/MetricWidget.d.ts +0 -2
  95. package/dist/types/react/ui/PeriodSummary.d.ts +0 -1
  96. package/dist/types/react/ui/PresentStory.d.ts +0 -1
  97. package/dist/types/react/ui/QueryList.d.ts +0 -1
  98. package/dist/types/react/ui/Ranking.d.ts +0 -1
  99. package/dist/types/react/ui/RefreshControl.d.ts +0 -1
  100. package/dist/types/react/ui/RefreshRegion.d.ts +0 -1
  101. package/dist/types/react/ui/RequestHint.d.ts +0 -1
  102. package/dist/types/react/ui/SearchField.d.ts +0 -1
  103. package/dist/types/react/ui/SearchInput.d.ts +1 -2
  104. package/dist/types/react/ui/SearchMatch.d.ts +0 -1
  105. package/dist/types/react/ui/SelectableBarChart.d.ts +0 -1
  106. package/dist/types/react/ui/SelectionMark.d.ts +0 -1
  107. package/dist/types/react/ui/Sheet.d.ts +0 -1
  108. package/dist/types/react/ui/Skeleton.d.ts +0 -1
  109. package/dist/types/react/ui/Stack.d.ts +0 -1
  110. package/dist/types/react/ui/StatusPanel.d.ts +0 -1
  111. package/dist/types/react/ui/TableWidget.d.ts +2 -3
  112. package/dist/types/react/ui/Tabs.d.ts +0 -1
  113. package/dist/types/react/ui/TextContent.d.ts +4 -0
  114. package/dist/types/react/ui/TextWidget.d.ts +19 -0
  115. package/dist/types/react/ui/ThemeSelector.d.ts +0 -1
  116. package/dist/types/react/ui/Tooltip.d.ts +0 -1
  117. package/dist/types/react/ui/UpdatedAt.d.ts +0 -1
  118. package/dist/types/react/ui/VariableBar.d.ts +0 -1
  119. package/dist/types/react/ui/VisualizationWidget.d.ts +5 -13
  120. package/dist/types/react/ui/WidgetDisclosure.d.ts +0 -1
  121. package/dist/types/react/ui/WidgetViewTabs.d.ts +2 -3
  122. package/dist/types/react/ui/data-identifiers.d.ts +0 -1
  123. package/dist/types/react/ui/presentation.d.ts +19 -0
  124. package/dist/types/react/ui/useAppAppearance.d.ts +3 -0
  125. package/dist/types/react/ui/useDataAppPresentation.d.ts +3 -0
  126. package/dist/types/react/view-controls.d.ts +2 -2
  127. package/dist/types/react/view.d.ts +2 -2
  128. package/dist/{bootstrap.js → worker.js} +144 -12
  129. package/docs/app-authoring.md +60 -24
  130. package/docs/client.md +48 -19
  131. package/docs/contract.md +10 -10
  132. package/docs/embed.md +94 -26
  133. package/docs/hosted-apps.md +39 -0
  134. package/docs/local-data-apps.md +14 -0
  135. package/docs/react-embed.md +75 -24
  136. package/docs/react.md +118 -93
  137. package/docs/server-bun.md +4 -1
  138. package/docs/server.md +2 -2
  139. package/docs/worker.md +55 -0
  140. package/examples/starter-data-app/index.tsx +159 -0
  141. package/package.json +18 -12
  142. package/dist/chunks/contract-14vxdcrs.js.map +0 -10
  143. package/dist/chunks/contract-7gpsee0v.js.map +0 -11
  144. package/dist/chunks/contract-d9skd1n7.js.map +0 -12
  145. package/dist/chunks/contract-ehk50k7e.js.map +0 -10
  146. package/dist/chunks/contract-ryyf6dme.js.map +0 -10
  147. package/dist/chunks/contract-zr4s2g7m.js.map +0 -12
  148. package/dist/react/index.css +0 -4478
  149. package/dist/react.css.d.ts +0 -1
  150. package/dist/types/embed/shell.d.ts +0 -21
  151. package/dist/types/embed/standalone.d.ts +0 -1
  152. package/dist/types/react/embed/shell.d.ts +0 -10
  153. package/dist/types/react/ui/index.d.ts +0 -129
  154. package/docs/appearance.md +0 -30
  155. package/docs/bootstrap.md +0 -76
  156. package/docs/config.md +0 -21
  157. package/docs/format.md +0 -29
  158. package/docs/react-styles.md +0 -17
  159. package/docs/starter-agent-instructions.md +0 -53
@@ -1,4 +1,4 @@
1
- import type { EmptyStateProps } from './ui/EmptyState.js';
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: Pick<EmptyStateProps, 'title' | 'description'>;
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
- async function queryData(operation, input, signal) {
264
- try {
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: queryData,
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("&", "&amp;").replaceAll('"', "&quot;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("'", "&#39;");
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
+ };
@@ -1,26 +1,62 @@
1
1
  # Author a data app
2
2
 
3
- An app owns its question, SQL, result parsing, business definitions, configuration,
4
- and presentation. The package supplies contracts, request handling, typed client
5
- calls, and reusable React UI.
6
-
7
- 1. Inspect the source data, time coverage, and existing definitions before choosing
8
- the exploration. Build findings from observed results and distinguish
9
- association from cause.
10
- 2. Define named, bounded [operations](contract.md) on the server. Put shared input
11
- contracts in a browser-safe module and validate outputs before returning them.
12
- 3. Use a [server handler](server.md) that authorizes each request, or the
13
- [Bun adapter](server-bun.md) for local development.
14
- 4. Create a typed [client](client.md) and compose [React views](react.md). Import
15
- the operation registry with `import type` in browser code.
16
- 5. Define glossary and query evidence, handle empty results, and preserve the
17
- displayed input while a request refreshes or fails. A measured zero and an
18
- unavailable value must remain distinct.
19
- 6. Verify the app's findings and interactions against the original question.
20
- Inspect loading, error, stale, and empty states at desktop and phone widths.
21
-
22
- Import through `@altertable/data-app/<entry>`. Installed package files are
23
- dependencies; customize the app's own source rather than editing `node_modules`.
24
- SQL, credentials, and viewer authorization belong on the server.
25
-
26
- For agent-assisted authoring, see the [starter AGENTS.md template](starter-agent-instructions.md).
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 only when the server permits
26
- SQL disclosure. Use the returned input when labeling stale data during a refresh.
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
- transport supports custom typed routes:
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
- The host independently limits pending requests to 128: an iframe can send
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. Keep SQL and operation implementations on the server; browser 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. The server records the SQL and query ID when execution occurs, so evidence does not need a separate result field. Browser modules import operation types with `import type`; they never import server implementations.
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