@altertable/data-app 0.59.1
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 +35 -0
- package/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/bootstrap.js +388 -0
- package/dist/chunks/contract-14vxdcrs.js +192 -0
- package/dist/chunks/contract-14vxdcrs.js.map +10 -0
- package/dist/chunks/contract-8q35dcyh.js +9 -0
- package/dist/chunks/contract-8q35dcyh.js.map +10 -0
- package/dist/chunks/contract-jksbmt5q.js +133 -0
- package/dist/chunks/contract-jksbmt5q.js.map +10 -0
- package/dist/chunks/contract-mb5nfzwg.js +375 -0
- package/dist/chunks/contract-mb5nfzwg.js.map +12 -0
- package/dist/chunks/contract-mev09s5v.js +77 -0
- package/dist/chunks/contract-mev09s5v.js.map +10 -0
- package/dist/chunks/contract-nt819swq.js +321 -0
- package/dist/chunks/contract-nt819swq.js.map +11 -0
- package/dist/chunks/contract-ryyf6dme.js +21 -0
- package/dist/chunks/contract-ryyf6dme.js.map +10 -0
- package/dist/chunks/contract-tkkc28tg.js +213 -0
- package/dist/chunks/contract-tkkc28tg.js.map +12 -0
- package/dist/chunks/contract-tqrf3ykr.js +232 -0
- package/dist/chunks/contract-tqrf3ykr.js.map +11 -0
- package/dist/chunks/contract-wz59z8pq.js +8 -0
- package/dist/chunks/contract-wz59z8pq.js.map +10 -0
- package/dist/client/index.js +27 -0
- package/dist/client/index.js.map +9 -0
- package/dist/core/appearance.js +12 -0
- package/dist/core/appearance.js.map +9 -0
- package/dist/core/config.js +8 -0
- package/dist/core/config.js.map +9 -0
- package/dist/core/contract.js +55 -0
- package/dist/core/contract.js.map +9 -0
- package/dist/core/format.js +18 -0
- package/dist/core/format.js.map +9 -0
- package/dist/embed/index.js +91 -0
- package/dist/embed/index.js.map +11 -0
- package/dist/local.js +332 -0
- package/dist/local.js.map +15 -0
- package/dist/react/embed/index.js +130 -0
- package/dist/react/embed/index.js.map +11 -0
- package/dist/react/index.css +4478 -0
- package/dist/react/index.js +6105 -0
- package/dist/react/index.js.map +90 -0
- package/dist/react.css.d.ts +1 -0
- package/dist/server.js +231 -0
- package/dist/server.js.map +14 -0
- package/dist/types/client/iframe.d.ts +39 -0
- package/dist/types/client/index.d.ts +42 -0
- package/dist/types/client/location.d.ts +15 -0
- package/dist/types/client/messages.d.ts +7 -0
- package/dist/types/client/navigation.d.ts +14 -0
- package/dist/types/client/transport.d.ts +15 -0
- package/dist/types/core/appearance.d.ts +28 -0
- package/dist/types/core/bridge.d.ts +35 -0
- package/dist/types/core/config.d.ts +14 -0
- package/dist/types/core/contract.d.ts +139 -0
- package/dist/types/core/data-view.d.ts +46 -0
- package/dist/types/core/date-range.d.ts +20 -0
- package/dist/types/core/dimension.d.ts +60 -0
- package/dist/types/core/format.d.ts +40 -0
- package/dist/types/core/invariant.d.ts +2 -0
- package/dist/types/core/messages.d.ts +77 -0
- package/dist/types/core/reading.d.ts +17 -0
- package/dist/types/core/variables.d.ts +82 -0
- package/dist/types/embed/bootstrap.d.ts +5 -0
- package/dist/types/embed/host.d.ts +23 -0
- package/dist/types/embed/index.d.ts +11 -0
- package/dist/types/embed/navigation.d.ts +6 -0
- package/dist/types/embed/shell.d.ts +21 -0
- package/dist/types/embed/standalone.d.ts +1 -0
- package/dist/types/react/content.d.ts +23 -0
- package/dist/types/react/embed/bridge.d.ts +12 -0
- package/dist/types/react/embed/index.d.ts +9 -0
- package/dist/types/react/embed/shell.d.ts +10 -0
- package/dist/types/react/hooks.d.ts +831 -0
- package/dist/types/react/index.d.ts +11 -0
- package/dist/types/react/mount.d.ts +11 -0
- package/dist/types/react/ui/AboutData.d.ts +61 -0
- package/dist/types/react/ui/AltertableLogo.d.ts +3 -0
- package/dist/types/react/ui/AppFooter.d.ts +7 -0
- package/dist/types/react/ui/AppHeader.d.ts +12 -0
- package/dist/types/react/ui/AppLayout.d.ts +17 -0
- package/dist/types/react/ui/AppScope.d.ts +8 -0
- package/dist/types/react/ui/AppToolbar.d.ts +32 -0
- package/dist/types/react/ui/Breakdown.d.ts +14 -0
- package/dist/types/react/ui/Button.d.ts +12 -0
- package/dist/types/react/ui/Checkbox.d.ts +11 -0
- package/dist/types/react/ui/Combobox.d.ts +43 -0
- package/dist/types/react/ui/ComparisonVisual.d.ts +15 -0
- package/dist/types/react/ui/ContentSkeleton.d.ts +8 -0
- package/dist/types/react/ui/DataApp.d.ts +56 -0
- package/dist/types/react/ui/DataBoundary.d.ts +16 -0
- package/dist/types/react/ui/DataSection.d.ts +28 -0
- package/dist/types/react/ui/DataTable.d.ts +28 -0
- package/dist/types/react/ui/DataViewToast.d.ts +12 -0
- package/dist/types/react/ui/DataWidget.d.ts +39 -0
- package/dist/types/react/ui/DateRangePicker.d.ts +29 -0
- package/dist/types/react/ui/DateTimeTooltip.d.ts +10 -0
- package/dist/types/react/ui/DimensionPicker.d.ts +11 -0
- package/dist/types/react/ui/EmptyState.d.ts +9 -0
- package/dist/types/react/ui/GettingStarted.d.ts +9 -0
- package/dist/types/react/ui/GlossaryDefinition.d.ts +8 -0
- package/dist/types/react/ui/GlossaryExplanation.d.ts +17 -0
- package/dist/types/react/ui/GradientScroll.d.ts +16 -0
- package/dist/types/react/ui/Grid.d.ts +13 -0
- package/dist/types/react/ui/GridItem.d.ts +7 -0
- package/dist/types/react/ui/HelpPopover.d.ts +24 -0
- package/dist/types/react/ui/IconButton.d.ts +19 -0
- package/dist/types/react/ui/InspectionContext.d.ts +10 -0
- package/dist/types/react/ui/Kbd.d.ts +8 -0
- package/dist/types/react/ui/LiveControl.d.ts +11 -0
- package/dist/types/react/ui/MetricWidget.d.ts +48 -0
- package/dist/types/react/ui/PeriodSummary.d.ts +17 -0
- package/dist/types/react/ui/PresentStory.d.ts +34 -0
- package/dist/types/react/ui/QueryList.d.ts +15 -0
- package/dist/types/react/ui/Ranking.d.ts +14 -0
- package/dist/types/react/ui/RefreshControl.d.ts +11 -0
- package/dist/types/react/ui/RefreshRegion.d.ts +10 -0
- package/dist/types/react/ui/RequestHint.d.ts +21 -0
- package/dist/types/react/ui/SearchField.d.ts +23 -0
- package/dist/types/react/ui/SearchInput.d.ts +9 -0
- package/dist/types/react/ui/SearchMatch.d.ts +7 -0
- package/dist/types/react/ui/SelectableBarChart.d.ts +16 -0
- package/dist/types/react/ui/SelectionMark.d.ts +5 -0
- package/dist/types/react/ui/Sheet.d.ts +19 -0
- package/dist/types/react/ui/Skeleton.d.ts +6 -0
- package/dist/types/react/ui/Stack.d.ts +8 -0
- package/dist/types/react/ui/StatusPanel.d.ts +11 -0
- package/dist/types/react/ui/TableWidget.d.ts +57 -0
- package/dist/types/react/ui/Tabs.d.ts +6 -0
- package/dist/types/react/ui/ThemeSelector.d.ts +13 -0
- package/dist/types/react/ui/Tooltip.d.ts +23 -0
- package/dist/types/react/ui/UpdatedAt.d.ts +10 -0
- package/dist/types/react/ui/VariableBar.d.ts +7 -0
- package/dist/types/react/ui/VisualizationWidget.d.ts +52 -0
- package/dist/types/react/ui/WidgetDisclosure.d.ts +7 -0
- package/dist/types/react/ui/WidgetEvidence.d.ts +12 -0
- package/dist/types/react/ui/WidgetViewTabs.d.ts +17 -0
- package/dist/types/react/ui/chartColor.d.ts +1 -0
- package/dist/types/react/ui/classNames.d.ts +1 -0
- package/dist/types/react/ui/comparison.d.ts +30 -0
- package/dist/types/react/ui/data-context.d.ts +65 -0
- package/dist/types/react/ui/data-identifiers.d.ts +33 -0
- package/dist/types/react/ui/icons.d.ts +42 -0
- package/dist/types/react/ui/index.d.ts +129 -0
- package/dist/types/react/ui/metric.d.ts +11 -0
- package/dist/types/react/ui/search.d.ts +6 -0
- package/dist/types/react/ui/searchItems.d.ts +34 -0
- package/dist/types/react/ui/shortcuts.d.ts +32 -0
- package/dist/types/react/ui/story.d.ts +20 -0
- package/dist/types/react/ui/variables.d.ts +36 -0
- package/dist/types/react/ui/widget-views.d.ts +3 -0
- package/dist/types/react/view-controls.d.ts +17 -0
- package/dist/types/react/view.d.ts +49 -0
- package/dist/types/server/handler.d.ts +13 -0
- package/dist/types/server/index.d.ts +7 -0
- package/dist/types/server/local.d.ts +17 -0
- package/docs/app-authoring.md +26 -0
- package/docs/appearance.md +30 -0
- package/docs/bootstrap.md +76 -0
- package/docs/client.md +127 -0
- package/docs/config.md +21 -0
- package/docs/contract.md +95 -0
- package/docs/embed.md +98 -0
- package/docs/format.md +29 -0
- package/docs/react-embed.md +57 -0
- package/docs/react-styles.md +17 -0
- package/docs/react.md +248 -0
- package/docs/releasing.md +74 -0
- package/docs/server-bun.md +26 -0
- package/docs/server.md +47 -0
- package/docs/starter-agent-instructions.md +51 -0
- package/package.json +124 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/core/contract.ts
|
|
3
|
+
import { parseDate, today } from "@internationalized/date";
|
|
4
|
+
|
|
5
|
+
// src/core/format.ts
|
|
6
|
+
var pluralRules = new Intl.PluralRules;
|
|
7
|
+
|
|
8
|
+
// src/core/invariant.ts
|
|
9
|
+
function invariant(condition, message) {
|
|
10
|
+
if (!condition)
|
|
11
|
+
throw new Error(`Invariant failed: ${message}`);
|
|
12
|
+
}
|
|
13
|
+
// src/core/messages.ts
|
|
14
|
+
var errorBrand = Symbol.for("@altertable/data-app/MessageRoutingError");
|
|
15
|
+
// src/core/contract.ts
|
|
16
|
+
function defineQueryNames(names) {
|
|
17
|
+
const values = Object.values(names);
|
|
18
|
+
invariant(values.every((name) => !!name.trim()) && new Set(values).size === values.length, "Query names must be nonempty and unique.");
|
|
19
|
+
return names;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class DataSourceError extends Error {
|
|
23
|
+
reason;
|
|
24
|
+
status;
|
|
25
|
+
queryName;
|
|
26
|
+
constructor(reason, status) {
|
|
27
|
+
super(`Data source ${reason}${status ? ` (${status})` : ""}.`);
|
|
28
|
+
this.reason = reason;
|
|
29
|
+
this.status = status;
|
|
30
|
+
this.name = "DataSourceError";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
var connectionQueryNames = defineQueryNames({
|
|
34
|
+
connection: "connection-check"
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// src/server/handler.ts
|
|
38
|
+
var sourceErrorMessages = {
|
|
39
|
+
unauthorized: "Lakehouse access needs attention. Run this app with 'altertable app dev', or run 'altertable login' and retry.",
|
|
40
|
+
forbidden: "This app cannot access its lakehouse data. Check the selected profile's permissions.",
|
|
41
|
+
rate_limited: "The lakehouse is busy. Wait a moment and retry.",
|
|
42
|
+
query_rejected: "A lakehouse query was rejected. Check the app's data operation.",
|
|
43
|
+
unavailable: "The lakehouse is unavailable. Check the connection and retry."
|
|
44
|
+
};
|
|
45
|
+
function createDataHandler(operations, authorize) {
|
|
46
|
+
async function handleDataRequest(request) {
|
|
47
|
+
const requestId = crypto.randomUUID();
|
|
48
|
+
const url = new URL(request.url);
|
|
49
|
+
if (!url.pathname.startsWith("/api/data/"))
|
|
50
|
+
return new Response("Not found", { status: 404 });
|
|
51
|
+
const encodedName = url.pathname.slice("/api/data/".length);
|
|
52
|
+
if (!encodedName || encodedName.includes("/"))
|
|
53
|
+
return problem(404, "not_found", "Unknown data operation.", requestId);
|
|
54
|
+
let name;
|
|
55
|
+
try {
|
|
56
|
+
name = decodeURIComponent(encodedName);
|
|
57
|
+
} catch {
|
|
58
|
+
return problem(404, "not_found", "Unknown data operation.", requestId);
|
|
59
|
+
}
|
|
60
|
+
if (!Object.hasOwn(operations, name))
|
|
61
|
+
return problem(404, "not_found", "Unknown data operation.", requestId);
|
|
62
|
+
if (request.method !== "POST")
|
|
63
|
+
return problem(405, "method_not_allowed", "Use POST.", requestId);
|
|
64
|
+
if (request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json")
|
|
65
|
+
return problem(415, "unsupported_media_type", "Use application/json.", requestId);
|
|
66
|
+
const origin = request.headers.get("origin");
|
|
67
|
+
const fetchSite = request.headers.get("sec-fetch-site");
|
|
68
|
+
const fetchMode = request.headers.get("sec-fetch-mode");
|
|
69
|
+
if (origin !== null && origin !== url.origin || fetchSite !== null && fetchSite !== "same-origin" || fetchSite !== null && origin === null || fetchMode !== null && fetchMode !== "cors" && fetchMode !== "same-origin")
|
|
70
|
+
return problem(403, "cross_origin_request", "The data request must come from this app.", requestId);
|
|
71
|
+
let access;
|
|
72
|
+
try {
|
|
73
|
+
access = await authorize(request, name);
|
|
74
|
+
} catch {
|
|
75
|
+
return problem(403, "forbidden", "You cannot run this data operation.", requestId);
|
|
76
|
+
}
|
|
77
|
+
const operation = operations[name];
|
|
78
|
+
let input;
|
|
79
|
+
if (request.signal.aborted)
|
|
80
|
+
return problem(504, "timeout", "The data request timed out.", requestId);
|
|
81
|
+
try {
|
|
82
|
+
const body = await readInput(request);
|
|
83
|
+
input = operation.input(JSON.parse(body));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error instanceof InputTooLargeError)
|
|
86
|
+
return problem(413, "input_too_large", "Input is too large.", requestId);
|
|
87
|
+
return problem(400, "invalid_input", "Check this operation's inputs.", requestId);
|
|
88
|
+
}
|
|
89
|
+
const timeout = AbortSignal.timeout(operation.policy.maxDurationMs);
|
|
90
|
+
const signal = AbortSignal.any([request.signal, timeout]);
|
|
91
|
+
const queryIds = [];
|
|
92
|
+
const queries = [];
|
|
93
|
+
const boundedLakehouse = {
|
|
94
|
+
async queryAll(statement, options) {
|
|
95
|
+
if (!Number.isInteger(options.limit) || options.limit < 1)
|
|
96
|
+
throw new Error("Query needs a positive row limit.");
|
|
97
|
+
if (operation.queryNames && !Object.values(operation.queryNames).includes(options.name ?? ""))
|
|
98
|
+
throw new Error(`Query name is not registered for operation ${name}.`);
|
|
99
|
+
const query = {
|
|
100
|
+
name: options.name ?? `Query ${queries.length + 1}`,
|
|
101
|
+
statement
|
|
102
|
+
};
|
|
103
|
+
queries.push(query);
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = await access.lakehouse.queryAll(statement, {
|
|
107
|
+
limit: Math.min(options.limit, operation.policy.maxQueryRows),
|
|
108
|
+
signal: AbortSignal.any([signal, options.signal])
|
|
109
|
+
});
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof DataSourceError)
|
|
112
|
+
error.queryName = query.name;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
if (result.rows.length > operation.policy.maxQueryRows)
|
|
116
|
+
throw new Error("Result exceeds row limit.");
|
|
117
|
+
if (result.queryId) {
|
|
118
|
+
query.queryId = result.queryId;
|
|
119
|
+
queryIds.push(result.queryId);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
let abort;
|
|
125
|
+
try {
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
const cancelled = new Promise((_, reject) => {
|
|
128
|
+
function rejectAborted() {
|
|
129
|
+
reject(new Error("Request aborted."));
|
|
130
|
+
}
|
|
131
|
+
abort = rejectAborted;
|
|
132
|
+
signal.addEventListener("abort", rejectAborted, { once: true });
|
|
133
|
+
if (signal.aborted)
|
|
134
|
+
rejectAborted();
|
|
135
|
+
});
|
|
136
|
+
const data = operation.output(await Promise.race([
|
|
137
|
+
cancelled,
|
|
138
|
+
Promise.resolve().then(() => {
|
|
139
|
+
signal.throwIfAborted();
|
|
140
|
+
return operation.run({ lakehouse: boundedLakehouse, signal }, input);
|
|
141
|
+
})
|
|
142
|
+
]));
|
|
143
|
+
const result = {
|
|
144
|
+
data,
|
|
145
|
+
requestId,
|
|
146
|
+
queriedAt: new Date().toISOString(),
|
|
147
|
+
queryIds,
|
|
148
|
+
...operation.policy.exposeSql && access.canDiscloseSql ? { queries } : {}
|
|
149
|
+
};
|
|
150
|
+
const body = JSON.stringify(result);
|
|
151
|
+
if (new TextEncoder().encode(body).byteLength > (operation.policy.maxResponseBytes ?? 1e6)) {
|
|
152
|
+
throw new Error("Response exceeds byte limit.");
|
|
153
|
+
}
|
|
154
|
+
return new Response(body, {
|
|
155
|
+
headers: {
|
|
156
|
+
"cache-control": "no-store",
|
|
157
|
+
"content-type": "application/json"
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const source = error instanceof DataSourceError ? error : null;
|
|
162
|
+
console.error(`Data operation ${name} failed (${requestId}): ${source ? `${source.reason}${source.status ? ` (${source.status})` : ""}${source.queryName ? ` in ${source.queryName}` : ""}` : error instanceof Error ? error.name : "unknown"}`);
|
|
163
|
+
if (source) {
|
|
164
|
+
return problem(source.reason === "rate_limited" ? 429 : 502, `source_${source.reason}`, sourceErrorMessages[source.reason], requestId);
|
|
165
|
+
}
|
|
166
|
+
return problem(signal.aborted ? 504 : 502, signal.aborted ? "timeout" : "query_failed", signal.aborted ? "The data request timed out." : "The data request failed.", requestId);
|
|
167
|
+
} finally {
|
|
168
|
+
if (abort)
|
|
169
|
+
signal.removeEventListener("abort", abort);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return handleDataRequest;
|
|
173
|
+
}
|
|
174
|
+
function problem(status, code, message, requestId) {
|
|
175
|
+
return Response.json({ error: { code, message, requestId } }, { status, headers: { "cache-control": "no-store" } });
|
|
176
|
+
}
|
|
177
|
+
var MAX_INPUT_BYTES = 16384;
|
|
178
|
+
|
|
179
|
+
class InputTooLargeError extends Error {
|
|
180
|
+
}
|
|
181
|
+
async function readInput(request) {
|
|
182
|
+
const reader = request.body?.getReader();
|
|
183
|
+
if (!reader)
|
|
184
|
+
return "";
|
|
185
|
+
const chunks = [];
|
|
186
|
+
let size = 0;
|
|
187
|
+
let abort;
|
|
188
|
+
function cancelReader() {
|
|
189
|
+
reader.cancel().catch(() => {});
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
request.signal.throwIfAborted();
|
|
193
|
+
const cancelled = new Promise((_, reject) => {
|
|
194
|
+
function rejectAborted() {
|
|
195
|
+
reject(new Error("Request aborted."));
|
|
196
|
+
cancelReader();
|
|
197
|
+
}
|
|
198
|
+
abort = rejectAborted;
|
|
199
|
+
request.signal.addEventListener("abort", rejectAborted, { once: true });
|
|
200
|
+
if (request.signal.aborted)
|
|
201
|
+
rejectAborted();
|
|
202
|
+
});
|
|
203
|
+
while (true) {
|
|
204
|
+
const { done, value } = await Promise.race([reader.read(), cancelled]);
|
|
205
|
+
if (done)
|
|
206
|
+
break;
|
|
207
|
+
size += value.byteLength;
|
|
208
|
+
if (size > MAX_INPUT_BYTES) {
|
|
209
|
+
cancelReader();
|
|
210
|
+
throw new InputTooLargeError;
|
|
211
|
+
}
|
|
212
|
+
chunks.push(value);
|
|
213
|
+
}
|
|
214
|
+
const bytes = new Uint8Array(size);
|
|
215
|
+
let offset = 0;
|
|
216
|
+
for (const chunk of chunks) {
|
|
217
|
+
bytes.set(chunk, offset);
|
|
218
|
+
offset += chunk.byteLength;
|
|
219
|
+
}
|
|
220
|
+
return new TextDecoder().decode(bytes);
|
|
221
|
+
} finally {
|
|
222
|
+
if (abort)
|
|
223
|
+
request.signal.removeEventListener("abort", abort);
|
|
224
|
+
reader.releaseLock();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export {
|
|
228
|
+
createDataHandler
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
//# debugId=14B5C03112DB3A8E64756E2164756E21
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/core/contract.ts", "../src/core/format.ts", "../src/core/invariant.ts", "../src/core/messages.ts", "../src/server/handler.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"/**\n * Shared operation contracts and input/output validation for browser and server.\n * @module @altertable/data-app/contract\n * @see https://github.com/altertable-ai/data-app/blob/main/docs/contract.md\n */\nimport { parseDate, today } from '@internationalized/date';\nimport { formatDateRange } from '@/src/core/format';\nimport { invariant } from '@/src/core/invariant';\n\nexport type QueryResult = {\n columns: { name: string; type?: string }[];\n rows: unknown[][];\n queryId?: string;\n};\n\nexport function parseEmptyInput(value: unknown): Record<string, never> {\n invariant(\n typeof value === 'object' &&\n value !== null &&\n !Array.isArray(value) &&\n Object.keys(value).length === 0,\n 'This operation takes no inputs.'\n );\n\n return {};\n}\n\nexport function parseTrue(value: unknown): true {\n invariant(value === true, 'Expected a successful check.');\n\n return true;\n}\n\nexport type DateRangeInput = { start: string; end: string };\nexport type DateRangeRequest = {\n range: DateRangeInput;\n comparison: DateRangeInput | null;\n};\n\n/** The immediately preceding, equally long set of calendar days. */\nexport function previousDateRange({\n start,\n end,\n}: DateRangeInput): DateRangeInput {\n const first = parseDate(start);\n const last = parseDate(end);\n const days =\n (last.toDate('UTC').getTime() - first.toDate('UTC').getTime()) /\n 86_400_000 +\n 1;\n invariant(days >= 1, 'Date range must start before it ends.');\n const previousEnd = first.subtract({ days: 1 });\n\n return {\n start: previousEnd.subtract({ days: days - 1 }).toString(),\n end: previousEnd.toString(),\n };\n}\n\nexport type ReportingPeriod =\n | { kind: 'rolling'; amount: number; unit: 'hour' | 'day'; end: string }\n | { kind: 'calendar'; start: string; end: string; timeZone: string };\n\nexport type DateRangeContractOptions = {\n minDate?: string;\n maxDate?: string | (() => string);\n maxRangeDays: number;\n timeZone: string;\n completeDays?: boolean;\n};\n\n/** One source policy for the server parser, URL variable, and displayed reporting period. */\nexport function defineDateRangeContract(options: DateRangeContractOptions) {\n invariant(\n Number.isInteger(options.maxRangeDays) && options.maxRangeDays > 0,\n 'Date range needs a positive maxRangeDays.'\n );\n if (options.minDate) parseDate(options.minDate);\n if (typeof options.maxDate === 'string') parseDate(options.maxDate);\n today(options.timeZone);\n\n function bounds() {\n const latest = today(options.timeZone).subtract({\n days: options.completeDays === false ? 0 : 1,\n });\n const sourceMax =\n typeof options.maxDate === 'function'\n ? options.maxDate()\n : options.maxDate;\n const maxDate =\n sourceMax && parseDate(sourceMax).compare(latest) < 0\n ? parseDate(sourceMax).toString()\n : latest.toString();\n\n return {\n minDate: options.minDate,\n maxDate,\n maxRangeDays: options.maxRangeDays,\n timeZone: options.timeZone,\n };\n }\n\n function period({ start, end }: DateRangeInput): ReportingPeriod {\n return {\n kind: 'calendar',\n start,\n end,\n timeZone: options.timeZone,\n };\n }\n\n function describeInput(input: DateRangeInput) {\n return `${formatDateRange(input)} ${options.timeZone}`;\n }\n\n function comparison(input: DateRangeInput): DateRangeInput | null {\n const previous = previousDateRange(input);\n\n return options.minDate && previous.start < options.minDate\n ? null\n : previous;\n }\n\n function request(value: unknown, compare = false): DateRangeRequest {\n const range = parseDateRangeInput(value, bounds());\n const previous = compare ? comparison(range) : null;\n invariant(\n !compare || previous,\n 'Comparison is outside available source coverage.'\n );\n\n return { range, comparison: previous };\n }\n\n function parseRequest(value: unknown): DateRangeRequest {\n invariant(value && typeof value === 'object', 'Choose a date range.');\n const input = value as Record<string, unknown>;\n const parsed = request(input.range, input.comparison != null);\n if (input.comparison != null) {\n const supplied = parseDateRangeInput(input.comparison, bounds());\n invariant(\n supplied.start === parsed.comparison?.start &&\n supplied.end === parsed.comparison?.end,\n 'Comparison must be the preceding equal-length range.'\n );\n }\n\n return parsed;\n }\n\n return {\n request,\n parseRequest,\n bounds,\n parse(value: unknown) {\n return parseDateRangeInput(value, bounds());\n },\n period,\n comparison,\n describeInput,\n view: { period, describeInput },\n };\n}\n\nexport type DateRangeContract = ReturnType<typeof defineDateRangeContract>;\n\n/** Validate calendar dates and a bounded inclusive range on the server. */\nexport function parseDateRangeInput(\n value: unknown,\n {\n minDate,\n maxDate,\n maxRangeDays,\n }: { minDate?: string; maxDate?: string; maxRangeDays: number }\n): DateRangeInput {\n invariant(\n value && typeof value === 'object' && !Array.isArray(value),\n 'Choose a date range.'\n );\n const { start, end } = value as Record<string, unknown>;\n\n function day(input: unknown): number {\n invariant(\n typeof input === 'string' && /^\\d{4}-\\d{2}-\\d{2}$/.test(input),\n 'Dates must be YYYY-MM-DD.'\n );\n const timestamp = Date.parse(`${input}T00:00:00Z`);\n invariant(\n Number.isFinite(timestamp) &&\n new Date(timestamp).toISOString().slice(0, 10) === input,\n 'Choose valid calendar dates.'\n );\n\n return timestamp;\n }\n const first = day(start);\n const last = day(end);\n invariant(\n Number.isInteger(maxRangeDays) && maxRangeDays > 0,\n 'Maximum range days must be positive.'\n );\n invariant(\n first <= last &&\n (last - first) / 86_400_000 < maxRangeDays &&\n (minDate === undefined || first >= day(minDate)) &&\n (maxDate === undefined || last <= day(maxDate)),\n `Choose up to ${maxRangeDays} days within the available range.`\n );\n\n return { start: start as string, end: end as string };\n}\n\n/** Accepts numeric strings from DuckDB; rejects negative, fractional, and unsafe integers. */\nexport function parseCount(value: unknown): number {\n const number =\n typeof value === 'number' || typeof value === 'string'\n ? Number(value)\n : NaN;\n invariant(\n Number.isSafeInteger(number) &&\n number >= 0 &&\n (typeof value !== 'string' || !!value.trim()),\n 'Invalid count in query result.'\n );\n\n return number;\n}\n\nexport function parseLabel(value: unknown, maxLength = 100): string {\n invariant(\n typeof value === 'string' && !!value && value.length <= maxLength,\n 'Invalid label in query result.'\n );\n\n return value;\n}\n\n/** Rejects missing or duplicate columns and rows whose width differs from the column list. */\nexport function rowsAsRecords(\n result: QueryResult,\n requiredColumns: readonly string[]\n): Record<string, unknown>[] {\n const names = result.columns.map(column => column.name);\n invariant(\n new Set(names).size === names.length &&\n requiredColumns.every(name => names.includes(name)),\n 'Query result columns do not match the expected shape.'\n );\n\n return result.rows.map(row => {\n invariant(\n row.length === names.length,\n 'Query result row has the wrong width.'\n );\n\n return Object.fromEntries(names.map((name, index) => [name, row[index]]));\n });\n}\n\n/** One named statement an operation ran. Returned only when SQL disclosure is allowed. */\nexport type DisclosedQuery = {\n name: string;\n statement: string;\n queryId?: string;\n};\n\nexport function defineQueryNames<const Names extends Record<string, string>>(\n names: Names\n): Names {\n const values = Object.values(names);\n invariant(\n values.every(name => !!name.trim()) &&\n new Set(values).size === values.length,\n 'Query names must be nonempty and unique.'\n );\n\n return names;\n}\n\nexport class DataSourceError extends Error {\n queryName?: string;\n\n constructor(\n public readonly reason:\n | 'unauthorized'\n | 'forbidden'\n | 'rate_limited'\n | 'query_rejected'\n | 'unavailable',\n public readonly status?: number\n ) {\n super(`Data source ${reason}${status ? ` (${status})` : ''}.`);\n this.name = 'DataSourceError';\n }\n}\n\n/** Server-only query interface supplied by a local or hosted adapter. */\nexport type Lakehouse = {\n queryAll(\n statement: string,\n options: { limit: number; signal: AbortSignal; name?: string }\n ): Promise<QueryResult>;\n};\n\nexport type OperationContext = { lakehouse: Lakehouse; signal: AbortSignal };\n\n/**\n * Both parsers run on the server before results cross the JSON boundary. Schema libraries can be\n * used inside either parser.\n */\nexport type DataOperation<Input, Output> = {\n input: (value: unknown) => Input;\n output: (value: unknown) => Output;\n run: (context: OperationContext, input: Input) => Promise<Output>;\n checks: readonly Input[];\n queryNames?: Readonly<Record<string, string>>;\n policy: {\n maxQueryRows: number;\n maxDurationMs: number;\n maxResponseBytes?: number;\n exposeSql?: boolean;\n };\n};\n\nexport type OperationQuery<Names extends Readonly<Record<string, string>>> = (\n name: Names[keyof Names],\n statement: string,\n options?: { limit?: number }\n) => Promise<QueryResult>;\n\nexport function defineOperation<\n Input,\n Output,\n const Names extends Readonly<Record<string, string>> = Record<string, never>,\n>(\n operation: Omit<DataOperation<Input, Output>, 'run' | 'queryNames'> & {\n queryNames?: Names;\n run: (\n context: OperationContext & { query: OperationQuery<NoInfer<Names>> },\n input: Input\n ) => Promise<Output>;\n }\n): DataOperation<Input, Output> & { queryNames?: Names } {\n invariant(\n Array.isArray(operation.checks) &&\n operation.checks.length > 0 &&\n Number.isInteger(operation.policy.maxQueryRows) &&\n operation.policy.maxQueryRows > 0 &&\n Number.isInteger(operation.policy.maxDurationMs) &&\n operation.policy.maxDurationMs > 0 &&\n (operation.policy.maxResponseBytes === undefined ||\n (Number.isInteger(operation.policy.maxResponseBytes) &&\n operation.policy.maxResponseBytes > 0)),\n 'Each data operation needs check inputs and positive row and duration limits.'\n );\n if (operation.queryNames) defineQueryNames(operation.queryNames);\n for (const input of operation.checks) operation.input(input);\n\n return {\n ...operation,\n run(context, input) {\n function query(\n name: Names[keyof Names],\n statement: string,\n options?: { limit?: number }\n ): Promise<QueryResult> {\n invariant(\n operation.queryNames &&\n Object.values(operation.queryNames).includes(name),\n `Unknown query name: ${name}.`\n );\n\n return context.lakehouse.queryAll(statement, {\n name,\n limit: options?.limit ?? operation.policy.maxQueryRows,\n signal: context.signal,\n });\n }\n\n return operation.run({ ...context, query }, input);\n },\n };\n}\n\nexport const connectionQueryNames = defineQueryNames({\n connection: 'connection-check',\n});\n\n/** Success requires a bounded SQL query; it does not establish access to a particular dataset. */\nexport function connectionCheck(): DataOperation<Record<string, never>, true> {\n return defineOperation({\n input: parseEmptyInput,\n output: parseTrue,\n checks: [{}],\n queryNames: connectionQueryNames,\n policy: { maxQueryRows: 1, maxDurationMs: 15_000, exposeSql: true },\n async run({ query }): Promise<true> {\n await query(\n connectionQueryNames.connection,\n 'SELECT 1 AS connection_check'\n );\n\n return true;\n },\n });\n}\n\nexport type DataOperations = Record<\n string,\n {\n input: (value: unknown) => unknown;\n output: (value: unknown) => unknown;\n run: (context: OperationContext, input: never) => Promise<unknown>;\n checks: readonly unknown[];\n queryNames?: Readonly<Record<string, string>>;\n policy: DataOperation<never, unknown>['policy'];\n }\n>;\nexport {\n dimensionFilter,\n parseDimensionSelection,\n parseFacetOptions,\n dimensionPredicate,\n} from '@/src/core/dimension';\nexport type {\n DimensionSelection,\n DimensionMember,\n DimensionOption,\n DimensionVariable,\n DimensionFilterOptions,\n} from '@/src/core/dimension';\n\nexport * from '@/src/core/messages';\nexport type { TransportResponse } from '@/src/core/bridge';\n",
|
|
6
|
+
"/**\n * Date and number formatting with explicit units and missing-value behavior.\n * @module @altertable/data-app/format\n * @see https://github.com/altertable-ai/data-app/blob/main/docs/format.md\n */\ntype CommonOptions = { locale?: string; missing?: string };\n\n/** Compact, unambiguous calendar dates for labels and request status. */\nexport function formatDateRange({\n start,\n end,\n}: {\n start: string;\n end: string;\n}): string {\n const first = new Date(`${start}T00:00:00Z`);\n const last = new Date(`${end}T00:00:00Z`);\n\n function format(date: Date, options: Intl.DateTimeFormatOptions) {\n return new Intl.DateTimeFormat('en-US', {\n ...options,\n timeZone: 'UTC',\n }).format(date);\n }\n\n function formatFullDate(date: Date) {\n return format(date, { month: 'short', day: 'numeric', year: 'numeric' });\n }\n\n if (start === end) return formatFullDate(first);\n if (first.getUTCFullYear() !== last.getUTCFullYear())\n return `${formatFullDate(first)}–${formatFullDate(last)}`;\n const firstMonth = format(first, { month: 'short' });\n const lastMonth = format(last, { month: 'short' });\n const firstDay = first.getUTCDate();\n const lastDay = last.getUTCDate();\n\n return firstMonth === lastMonth\n ? `${firstMonth} ${firstDay}–${lastDay}, ${last.getUTCFullYear()}`\n : `${firstMonth} ${firstDay}–${lastMonth} ${lastDay}, ${last.getUTCFullYear()}`;\n}\n\nexport type MetricFormat =\n | { kind: 'count'; compact?: boolean; locale?: string }\n | { kind: 'ratio'; maximumFractionDigits?: number; locale?: string }\n | { kind: 'currency'; currency: string; locale?: string };\n\nexport function formatMetric(value: number, format: MetricFormat): string {\n if (format.kind === 'count') return formatCount(value, format);\n if (format.kind === 'ratio') return formatPercent(value, format);\n\n return formatNumber(value, {\n style: 'currency',\n currency: format.currency,\n locale: format.locale,\n });\n}\n\n/** Missing and non-finite values use the `missing` label; negative zero renders as zero. */\nexport function formatNumber(\n value: number | null | undefined,\n options: CommonOptions & Intl.NumberFormatOptions = {}\n): string {\n const { locale = 'en-US', missing = '—', ...numberOptions } = options;\n\n return value === null || value === undefined || !Number.isFinite(value)\n ? missing\n : new Intl.NumberFormat(locale, numberOptions).format(\n Object.is(value, -0) ? 0 : value\n );\n}\n\n/** Negative and fractional counts render as missing. Compact notation is opt-in. */\nexport function formatCount(\n value: number | null | undefined,\n options: CommonOptions & { compact?: boolean } = {}\n): string {\n const { compact = false, ...common } = options;\n if (\n value !== null &&\n value !== undefined &&\n (!Number.isInteger(value) || value < 0)\n ) {\n return common.missing ?? '—';\n }\n\n return formatNumber(value, {\n ...common,\n notation: compact ? 'compact' : 'standard',\n maximumFractionDigits: compact ? 1 : 0,\n });\n}\n\nconst pluralRules = new Intl.PluralRules();\n\nexport function pluralize(\n count: number,\n singular: string,\n plural = `${singular}s`\n): string {\n return pluralRules.select(count) === 'one' ? singular : plural;\n}\n\n/** Accepts a ratio (0.116), not a percentage (11.6). */\nexport function formatPercent(\n ratio: number | null | undefined,\n options: CommonOptions & { maximumFractionDigits?: number } = {}\n): string {\n if (ratio === null || ratio === undefined || !Number.isFinite(ratio)) {\n return options.missing ?? '—';\n }\n const digits =\n options.maximumFractionDigits ?? (Math.abs(ratio) < 0.01 ? 2 : 1);\n const percentOptions = {\n locale: options.locale,\n style: 'percent' as const,\n maximumFractionDigits: digits,\n };\n const smallestVisibleRatio = 10 ** -digits / 100;\n if (ratio !== 0 && Math.round(Math.abs(ratio) / smallestVisibleRatio) === 0) {\n const boundary = formatNumber(smallestVisibleRatio, percentOptions);\n\n return ratio > 0 ? `<${boundary}` : `>−${boundary}`;\n }\n\n return formatNumber(ratio, percentOptions);\n}\n",
|
|
7
|
+
"/** Assert an author or data contract while preserving a useful failure message. */\nexport function invariant(\n condition: unknown,\n message: string\n): asserts condition {\n if (!condition) throw new Error(`Invariant failed: ${message}`);\n}\n",
|
|
8
|
+
"import type { TransportResponse } from '@/src/core/bridge';\nimport type { DisclosedQuery } from '@/src/core/contract';\n\nexport type MessageContext = { signal: AbortSignal };\nexport type RoutedMessage = { route: string; payload: unknown };\nexport type MessageDispatcher = (\n message: unknown,\n context: MessageContext\n) => Promise<unknown>;\nexport type MessageTransport = (\n message: RoutedMessage,\n signal?: AbortSignal\n) => Promise<unknown>;\nexport type MessageRoute<Input, Output> = {\n input: (value: unknown) => Input;\n output: (value: unknown, input: Input) => Output;\n};\nexport type MessageRoutes = Record<\n string,\n {\n input: (value: unknown) => unknown;\n output: (value: unknown, input: never) => unknown;\n }\n>;\nexport type MessageInput<Route extends MessageRoutes[string]> = ReturnType<\n Route['input']\n>;\nexport type MessageHandlers<Routes extends MessageRoutes> = {\n [Name in keyof Routes]: (\n payload: MessageInput<Routes[Name]>,\n context: MessageContext\n ) =>\n | ReturnType<Routes[Name]['output']>\n | Promise<ReturnType<Routes[Name]['output']>>;\n};\n\n// A bootstrap and its app may bundle independent copies in the same window.\nconst errorBrand = Symbol.for('@altertable/data-app/MessageRoutingError');\n\n/** Only these deliberate, public failures cross the message boundary. Other errors stay private. */\nexport class MessageRoutingError extends Error {\n readonly [errorBrand] = true;\n\n static [Symbol.hasInstance](value: unknown): boolean {\n return (\n value instanceof Error &&\n (value as { [errorBrand]?: unknown })[errorBrand] === true\n );\n }\n\n constructor(\n public readonly code: string,\n message: string,\n public readonly requestId?: string\n ) {\n super(message);\n this.name = 'MessageRoutingError';\n }\n}\n\nexport function defineMessageRoute<Input, Output>(\n route: MessageRoute<Input, Output>\n): MessageRoute<Input, Output> {\n return route;\n}\n\n/** Contracts are shared; handlers are supplied by the host and never belong in the app bundle. */\nexport function createMessageRouter<const Routes extends MessageRoutes>(\n routes: Routes,\n handlers: MessageHandlers<NoInfer<Routes>>\n) {\n const registered = new Map(\n Object.entries(routes).map(([name, contract]) => {\n const handler = handlers[name];\n if (!name || name.length > 256 || typeof handler !== 'function')\n throw new Error(\n 'Each message route requires a handler and a bounded name.'\n );\n\n return [name, { contract, handler }] as const;\n })\n );\n\n async function dispatch(\n value: unknown,\n context: MessageContext\n ): Promise<unknown> {\n if (\n !value ||\n typeof value !== 'object' ||\n typeof (value as RoutedMessage).route !== 'string'\n )\n throw new MessageRoutingError(\n 'invalid_message',\n 'Invalid routed message.'\n );\n const message = value as RoutedMessage;\n const entry = registered.get(message.route);\n if (!entry)\n throw new MessageRoutingError('unknown_route', 'Unknown message route.');\n let payload: unknown;\n try {\n payload = entry.contract.input(message.payload);\n } catch {\n throw new MessageRoutingError(\n 'invalid_payload',\n 'Invalid message payload.'\n );\n }\n context.signal.throwIfAborted();\n let result: unknown;\n try {\n result = await entry.handler(payload as never, context);\n } catch (error) {\n if (context.signal.aborted) throw context.signal.reason;\n if (error instanceof MessageRoutingError) throw error;\n throw new MessageRoutingError(\n 'request_failed',\n 'The message request failed. Retry the request.'\n );\n }\n context.signal.throwIfAborted();\n try {\n return entry.contract.output(result, payload as never);\n } catch (error) {\n if (error instanceof MessageRoutingError) throw error;\n throw new MessageRoutingError(\n 'invalid_response',\n 'Invalid message response.'\n );\n }\n }\n\n return { dispatch };\n}\n\nexport type OperationContracts = Record<\n string,\n {\n input: (value: unknown) => unknown;\n output: (value: unknown) => unknown;\n }\n>;\nexport type DataQueryInput<Operations extends OperationContracts> = {\n [Name in keyof Operations & string]: {\n operation: Name;\n input: ReturnType<Operations[Name]['input']>;\n };\n}[keyof Operations & string];\nexport type DataQueryBody<Output> = {\n data: Output;\n requestId: string;\n queriedAt: string;\n queryIds: string[];\n queries?: DisclosedQuery[];\n};\nexport type DataQueryRoute<Operations extends OperationContracts> =\n MessageRoute<DataQueryInput<Operations>, TransportResponse> & {\n operations: Operations | undefined;\n };\nexport type MessageOutput<Route extends MessageRoutes[string], Input> =\n Route extends DataQueryRoute<infer Operations>\n ? Input extends { operation: infer Name extends keyof Operations }\n ? {\n status: number;\n body: DataQueryBody<ReturnType<Operations[Name]['output']>>;\n }\n : never\n : ReturnType<Route['output']>;\n\n/** Omit contracts only in generic hosts such as the CLI shell, where Bun validates operations. */\nexport function defineDataQueryRoute<\n Operations extends OperationContracts = OperationContracts,\n>(operations?: Operations): DataQueryRoute<Operations> {\n return {\n operations,\n input(value) {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid query.');\n const request = value as { operation?: unknown; input?: unknown };\n if (\n typeof request.operation !== 'string' ||\n !request.operation ||\n request.operation.length > 256 ||\n request.operation.includes('/')\n )\n throw new Error('Invalid operation.');\n const operation =\n operations && Object.hasOwn(operations, request.operation)\n ? operations[request.operation]\n : undefined;\n if (operations && !operation) throw new Error('Unknown operation.');\n\n return {\n operation: request.operation,\n input: operation ? operation.input(request.input) : request.input,\n } as DataQueryInput<Operations>;\n },\n output(value, input) {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid data response.');\n const response = value as TransportResponse;\n if (\n !Number.isInteger(response.status) ||\n response.status < 100 ||\n response.status > 599 ||\n !response.body ||\n typeof response.body !== 'object'\n )\n throw new Error('Invalid data response.');\n const body = response.body as DataQueryBody<unknown> & {\n error?: { code?: unknown; message?: unknown; requestId?: unknown };\n };\n if (response.status < 200 || response.status >= 300) {\n const failure = body.error;\n throw new MessageRoutingError(\n typeof failure?.code === 'string' ? failure.code : 'request_failed',\n typeof failure?.message === 'string'\n ? failure.message\n : 'Could not load data.',\n typeof failure?.requestId === 'string' ? failure.requestId : undefined\n );\n }\n if (\n !('data' in body) ||\n typeof body.requestId !== 'string' ||\n typeof body.queriedAt !== 'string' ||\n !Array.isArray(body.queryIds) ||\n !body.queryIds.every(id => typeof id === 'string')\n )\n throw new Error('Invalid data envelope.');\n if (\n body.queries !== undefined &&\n (!Array.isArray(body.queries) ||\n !body.queries.every(\n query =>\n query &&\n typeof query === 'object' &&\n typeof query.name === 'string' &&\n typeof query.statement === 'string' &&\n (query.queryId === undefined || typeof query.queryId === 'string')\n ))\n )\n throw new Error('Invalid query evidence.');\n const operation = operations?.[input.operation];\n\n return {\n status: response.status,\n body: {\n ...body,\n data: operation ? operation.output(body.data) : body.data,\n },\n };\n },\n };\n}\n\nexport type NavigationUpdate = {\n search: string;\n hash: string;\n mode: 'push' | 'replace';\n title?: string;\n};\nexport const navigationUpdateRoute = /* @__PURE__ */ defineMessageRoute({\n input(value: unknown): NavigationUpdate {\n if (!value || typeof value !== 'object')\n throw new Error('Invalid navigation.');\n const input = value as NavigationUpdate;\n if (\n typeof input.search !== 'string' ||\n input.search.length > 16_384 ||\n (input.search !== '' && !input.search.startsWith('?')) ||\n typeof input.hash !== 'string' ||\n input.hash.length > 4096 ||\n (input.hash !== '' && !input.hash.startsWith('#')) ||\n (input.mode !== 'push' && input.mode !== 'replace') ||\n (input.title !== undefined &&\n (typeof input.title !== 'string' || input.title.length > 512))\n )\n throw new Error('Invalid navigation.');\n\n return {\n search: input.search,\n hash: input.hash,\n mode: input.mode,\n ...(input.title === undefined ? {} : { title: input.title }),\n };\n },\n output(value: unknown): null {\n if (value !== null) throw new Error('Invalid navigation response.');\n\n return null;\n },\n});\nexport const dataAppRoutes = {\n 'data.query': /* @__PURE__ */ defineDataQueryRoute(),\n 'navigation.update': navigationUpdateRoute,\n};\n",
|
|
9
|
+
"import {\n DataSourceError,\n type DataOperations,\n type DisclosedQuery,\n type Lakehouse,\n} from '@/src/core/contract';\n\nconst sourceErrorMessages = {\n unauthorized:\n \"Lakehouse access needs attention. Run this app with 'altertable app dev', or run 'altertable login' and retry.\",\n forbidden:\n \"This app cannot access its lakehouse data. Check the selected profile's permissions.\",\n rate_limited: 'The lakehouse is busy. Wait a moment and retry.',\n query_rejected:\n \"A lakehouse query was rejected. Check the app's data operation.\",\n unavailable: 'The lakehouse is unavailable. Check the connection and retry.',\n};\n\n/** Authorization result for one viewer request. A hosted adapter must scope its lakehouse client. */\nexport type RequestAccess = { lakehouse: Lakehouse; canDiscloseSql: boolean };\n\n/**\n * Execute named operations on the server. The app supplies operations and viewer authorization.\n * JSON, Origin, and Fetch Metadata checks reject cross-site browser POSTs; they do not\n * authenticate another local process. SQL is returned only when both the operation and\n * authorization result permit disclosure.\n */\nexport function createDataHandler(\n operations: DataOperations,\n authorize: (request: Request, operation: string) => Promise<RequestAccess>\n) {\n async function handleDataRequest(request: Request): Promise<Response> {\n const requestId = crypto.randomUUID();\n const url = new URL(request.url);\n if (!url.pathname.startsWith('/api/data/'))\n return new Response('Not found', { status: 404 });\n const encodedName = url.pathname.slice('/api/data/'.length);\n if (!encodedName || encodedName.includes('/'))\n return problem(404, 'not_found', 'Unknown data operation.', requestId);\n let name: string;\n try {\n name = decodeURIComponent(encodedName);\n } catch {\n return problem(404, 'not_found', 'Unknown data operation.', requestId);\n }\n if (!Object.hasOwn(operations, name))\n return problem(404, 'not_found', 'Unknown data operation.', requestId);\n if (request.method !== 'POST')\n return problem(405, 'method_not_allowed', 'Use POST.', requestId);\n if (\n request.headers\n .get('content-type')\n ?.split(';')[0]\n ?.trim()\n .toLowerCase() !== 'application/json'\n )\n return problem(\n 415,\n 'unsupported_media_type',\n 'Use application/json.',\n requestId\n );\n const origin = request.headers.get('origin');\n const fetchSite = request.headers.get('sec-fetch-site');\n const fetchMode = request.headers.get('sec-fetch-mode');\n if (\n (origin !== null && origin !== url.origin) ||\n (fetchSite !== null && fetchSite !== 'same-origin') ||\n (fetchSite !== null && origin === null) ||\n (fetchMode !== null &&\n fetchMode !== 'cors' &&\n fetchMode !== 'same-origin')\n )\n return problem(\n 403,\n 'cross_origin_request',\n 'The data request must come from this app.',\n requestId\n );\n let access: RequestAccess;\n try {\n access = await authorize(request, name);\n } catch {\n return problem(\n 403,\n 'forbidden',\n 'You cannot run this data operation.',\n requestId\n );\n }\n const operation = operations[name]!;\n let input: unknown;\n if (request.signal.aborted)\n return problem(504, 'timeout', 'The data request timed out.', requestId);\n try {\n const body = await readInput(request);\n input = operation.input(JSON.parse(body));\n } catch (error) {\n if (error instanceof InputTooLargeError)\n return problem(\n 413,\n 'input_too_large',\n 'Input is too large.',\n requestId\n );\n\n return problem(\n 400,\n 'invalid_input',\n \"Check this operation's inputs.\",\n requestId\n );\n }\n const timeout = AbortSignal.timeout(operation.policy.maxDurationMs);\n const signal = AbortSignal.any([request.signal, timeout]);\n const queryIds: string[] = [];\n const queries: DisclosedQuery[] = [];\n const boundedLakehouse: Lakehouse = {\n async queryAll(statement, options) {\n if (!Number.isInteger(options.limit) || options.limit < 1)\n throw new Error('Query needs a positive row limit.');\n if (\n operation.queryNames &&\n !Object.values(operation.queryNames).includes(options.name ?? '')\n )\n throw new Error(\n `Query name is not registered for operation ${name}.`\n );\n const query: DisclosedQuery = {\n name: options.name ?? `Query ${queries.length + 1}`,\n statement,\n };\n queries.push(query);\n let result;\n try {\n result = await access.lakehouse.queryAll(statement, {\n limit: Math.min(options.limit, operation.policy.maxQueryRows),\n signal: AbortSignal.any([signal, options.signal]),\n });\n } catch (error) {\n if (error instanceof DataSourceError) error.queryName = query.name;\n throw error;\n }\n if (result.rows.length > operation.policy.maxQueryRows)\n throw new Error('Result exceeds row limit.');\n if (result.queryId) {\n query.queryId = result.queryId;\n queryIds.push(result.queryId);\n }\n\n return result;\n },\n };\n let abort: (() => void) | undefined;\n try {\n signal.throwIfAborted();\n const cancelled = new Promise<never>((_, reject) => {\n function rejectAborted() {\n reject(new Error('Request aborted.'));\n }\n abort = rejectAborted;\n signal.addEventListener('abort', rejectAborted, { once: true });\n if (signal.aborted) rejectAborted();\n });\n const data = operation.output(\n await Promise.race([\n cancelled,\n Promise.resolve().then(() => {\n signal.throwIfAborted();\n\n return operation.run(\n { lakehouse: boundedLakehouse, signal },\n input as never\n );\n }),\n ])\n );\n const result = {\n data,\n requestId,\n queriedAt: new Date().toISOString(),\n queryIds,\n ...(operation.policy.exposeSql && access.canDiscloseSql\n ? { queries }\n : {}),\n };\n const body = JSON.stringify(result);\n if (\n new TextEncoder().encode(body).byteLength >\n (operation.policy.maxResponseBytes ?? 1_000_000)\n ) {\n throw new Error('Response exceeds byte limit.');\n }\n\n return new Response(body, {\n headers: {\n 'cache-control': 'no-store',\n 'content-type': 'application/json',\n },\n });\n } catch (error) {\n const source = error instanceof DataSourceError ? error : null;\n console.error(\n `Data operation ${name} failed (${requestId}): ${source ? `${source.reason}${source.status ? ` (${source.status})` : ''}${source.queryName ? ` in ${source.queryName}` : ''}` : error instanceof Error ? error.name : 'unknown'}`\n );\n if (source) {\n return problem(\n source.reason === 'rate_limited' ? 429 : 502,\n `source_${source.reason}`,\n sourceErrorMessages[source.reason],\n requestId\n );\n }\n\n return problem(\n signal.aborted ? 504 : 502,\n signal.aborted ? 'timeout' : 'query_failed',\n signal.aborted\n ? 'The data request timed out.'\n : 'The data request failed.',\n requestId\n );\n } finally {\n if (abort) signal.removeEventListener('abort', abort);\n }\n }\n\n return handleDataRequest;\n}\n\nfunction problem(\n status: number,\n code: string,\n message: string,\n requestId: string\n): Response {\n return Response.json(\n { error: { code, message, requestId } },\n { status, headers: { 'cache-control': 'no-store' } }\n );\n}\n\nconst MAX_INPUT_BYTES = 16_384;\nclass InputTooLargeError extends Error {}\n\n/** Count encoded bytes before retaining chunks; cancellation also interrupts waiting reads. */\nasync function readInput(request: Request): Promise<string> {\n const reader = request.body?.getReader();\n if (!reader) return '';\n const chunks: Uint8Array[] = [];\n let size = 0;\n let abort: (() => void) | undefined;\n\n function cancelReader() {\n // The source's cancel algorithm may never settle; the handler must still respond.\n void reader!.cancel().catch(() => {});\n }\n\n try {\n request.signal.throwIfAborted();\n const cancelled = new Promise<never>((_, reject) => {\n function rejectAborted() {\n reject(new Error('Request aborted.'));\n cancelReader();\n }\n abort = rejectAborted;\n request.signal.addEventListener('abort', rejectAborted, { once: true });\n if (request.signal.aborted) rejectAborted();\n });\n while (true) {\n const { done, value } = await Promise.race([reader.read(), cancelled]);\n if (done) break;\n size += value.byteLength;\n if (size > MAX_INPUT_BYTES) {\n cancelReader();\n throw new InputTooLargeError();\n }\n chunks.push(value);\n }\n const bytes = new Uint8Array(size);\n let offset = 0;\n for (const chunk of chunks) {\n bytes.set(chunk, offset);\n offset += chunk.byteLength;\n }\n\n return new TextDecoder().decode(bytes);\n } finally {\n if (abort) request.signal.removeEventListener('abort', abort);\n reader.releaseLock();\n }\n}\n"
|
|
10
|
+
],
|
|
11
|
+
"mappings": "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",
|
|
12
|
+
"debugId": "14B5C03112DB3A8E64756E2164756E21",
|
|
13
|
+
"names": []
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type TransportResponse } from '../core/bridge.js';
|
|
2
|
+
import { type RoutedMessage } from '../core/messages.js';
|
|
3
|
+
/** One bridge per document, shared by all clients and retained across module hot replacement. */
|
|
4
|
+
export declare function createIframeTransport({ parentOrigin, window: frame, timeoutMs, mode, loadScript, }: {
|
|
5
|
+
parentOrigin: string;
|
|
6
|
+
window?: Window;
|
|
7
|
+
timeoutMs?: number;
|
|
8
|
+
mode?: 'url' | 'bundle';
|
|
9
|
+
loadScript?: (javascript: string) => void;
|
|
10
|
+
}): {
|
|
11
|
+
request: (message: RoutedMessage, signal?: AbortSignal) => Promise<unknown>;
|
|
12
|
+
transport: (operation: string, input: unknown, signal?: AbortSignal) => Promise<TransportResponse>;
|
|
13
|
+
dispose: () => void;
|
|
14
|
+
mode: "bundle" | "url";
|
|
15
|
+
snapshot(): unknown;
|
|
16
|
+
subscribe(listener: (state: unknown) => void): () => void;
|
|
17
|
+
ready(): void;
|
|
18
|
+
fail(): void;
|
|
19
|
+
};
|
|
20
|
+
export type IframeTransport = ReturnType<typeof createIframeTransport>;
|
|
21
|
+
type LocalBridge = Pick<ReturnType<typeof createIframeTransport>, 'transport'> & {
|
|
22
|
+
connect?: () => Promise<ReturnType<typeof createIframeTransport>>;
|
|
23
|
+
};
|
|
24
|
+
/** The URL opts into local preview; only same-origin server configuration establishes trust. */
|
|
25
|
+
export declare function localFrameBridge(window?: Window): LocalBridge | undefined;
|
|
26
|
+
/** Install an explicitly trusted transport for hosted apps and their URL-backed controls. */
|
|
27
|
+
export declare function installDataAppTransport(bridge: ReturnType<typeof createIframeTransport>, frame?: Window): () => void;
|
|
28
|
+
/** The bootstrap installs this before executing app code. URL apps can install an explicit connection. */
|
|
29
|
+
export declare function getDataAppTransport(frame?: Window): {
|
|
30
|
+
request: (message: RoutedMessage, signal?: AbortSignal) => Promise<unknown>;
|
|
31
|
+
transport: (operation: string, input: unknown, signal?: AbortSignal) => Promise<TransportResponse>;
|
|
32
|
+
dispose: () => void;
|
|
33
|
+
mode: "bundle" | "url";
|
|
34
|
+
snapshot(): unknown;
|
|
35
|
+
subscribe(listener: (state: unknown) => void): () => void;
|
|
36
|
+
ready(): void;
|
|
37
|
+
fail(): void;
|
|
38
|
+
} | undefined;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed clients for HTTP and iframe delivery; safe to import in the browser.
|
|
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';
|
|
11
|
+
export type InputOf<T> = T extends {
|
|
12
|
+
input: (value: unknown) => infer Input;
|
|
13
|
+
} ? Input : never;
|
|
14
|
+
export type OutputOf<T> = T extends {
|
|
15
|
+
output: (value: unknown) => infer Output;
|
|
16
|
+
} ? Output : never;
|
|
17
|
+
/**
|
|
18
|
+
* Parsed operation data and query evidence. `queries` is present only when SQL disclosure is
|
|
19
|
+
* allowed.
|
|
20
|
+
*/
|
|
21
|
+
export type DataResponse<Output, Input = unknown> = {
|
|
22
|
+
data: Output;
|
|
23
|
+
/** The exact browser input that produced this response. Never infer it from current controls. */
|
|
24
|
+
input: Input;
|
|
25
|
+
requestId: string;
|
|
26
|
+
queriedAt: string;
|
|
27
|
+
queryIds: string[];
|
|
28
|
+
queries?: DisclosedQuery[];
|
|
29
|
+
};
|
|
30
|
+
export type DataClient<Operations extends DataOperations> = {
|
|
31
|
+
query<Name extends keyof Operations & string>(name: Name, input: InputOf<Operations[Name]>, options?: {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
}): Promise<DataResponse<OutputOf<Operations[Name]>, InputOf<Operations[Name]>>>;
|
|
34
|
+
};
|
|
35
|
+
/** Browser client for named operations; it sends inputs, never SQL or lakehouse credentials. */
|
|
36
|
+
export declare function createDataClient<Operations extends DataOperations>(options?: {
|
|
37
|
+
transport?: DataTransport;
|
|
38
|
+
endpoint?: string;
|
|
39
|
+
fetch?: typeof fetch;
|
|
40
|
+
}): DataClient<Operations>;
|
|
41
|
+
export type { DataAppLocation, AppLocation } from './location.js';
|
|
42
|
+
export { createDataAppNavigation, getDataAppNavigation, type DataAppNavigation, } from './navigation.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type AppLocation = {
|
|
2
|
+
search: string;
|
|
3
|
+
hash: string;
|
|
4
|
+
};
|
|
5
|
+
export type DataAppLocation = {
|
|
6
|
+
snapshot(): AppLocation;
|
|
7
|
+
subscribe(listener: () => void): () => void;
|
|
8
|
+
update(location: AppLocation, mode?: 'push' | 'replace'): void;
|
|
9
|
+
};
|
|
10
|
+
/** Opaque sandboxes keep app state in memory; URL frames retain their real URL for HMR. */
|
|
11
|
+
export declare function createAppLocation(frame: Window, virtual: boolean, publish: (location: AppLocation, mode: 'push' | 'replace') => void): {
|
|
12
|
+
location: DataAppLocation;
|
|
13
|
+
apply: (next: AppLocation) => void;
|
|
14
|
+
dispose: () => void;
|
|
15
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type MessageInput, type MessageOutput, type MessageRoutes, type MessageTransport } from '../core/messages.js';
|
|
2
|
+
/** Both ends validate using the same contracts; the transport owns delivery and cancellation. */
|
|
3
|
+
export declare function createMessageClient<const Routes extends MessageRoutes>(routes: Routes, transport: MessageTransport): {
|
|
4
|
+
request<Name extends keyof Routes & string, const Input extends MessageInput<NoInfer<Routes[Name]>>>(route: Name, payload: Input, { signal }?: {
|
|
5
|
+
signal?: AbortSignal;
|
|
6
|
+
}): Promise<MessageOutput<Routes[Name], Input>>;
|
|
7
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type DataAppLocation } from './location.js';
|
|
2
|
+
import { type IframeTransport } from './iframe.js';
|
|
3
|
+
export type DataAppNavigation = DataAppLocation & {
|
|
4
|
+
/** Publish URL changes made outside the adapter, including the document title. */
|
|
5
|
+
publish(mode?: 'push' | 'replace'): void;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
};
|
|
8
|
+
/** Attach app URL behavior to an already authenticated bridge. */
|
|
9
|
+
export declare function createDataAppNavigation({ bridge, window: frame, }: {
|
|
10
|
+
bridge: IframeTransport;
|
|
11
|
+
window?: Window;
|
|
12
|
+
}): DataAppNavigation;
|
|
13
|
+
/** URL controls attach navigation on demand; bootstrap and data-only clients do not. */
|
|
14
|
+
export declare function getDataAppNavigation(frame?: Window): DataAppNavigation | undefined;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { TransportResponse } from '../core/bridge.js';
|
|
2
|
+
export type DataTransport = (operation: string, input: unknown, signal?: AbortSignal) => Promise<TransportResponse>;
|
|
3
|
+
declare const errorBrand: unique symbol;
|
|
4
|
+
export declare class DataAppError extends Error {
|
|
5
|
+
readonly code: string;
|
|
6
|
+
readonly requestId?: string | undefined;
|
|
7
|
+
readonly [errorBrand] = true;
|
|
8
|
+
static [Symbol.hasInstance](value: unknown): boolean;
|
|
9
|
+
constructor(message: string, code: string, requestId?: string | undefined);
|
|
10
|
+
}
|
|
11
|
+
export declare function createHttpTransport({ endpoint, fetch: request, }?: {
|
|
12
|
+
endpoint?: string;
|
|
13
|
+
fetch?: typeof fetch;
|
|
14
|
+
}): DataTransport;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type AppearanceSettings = {
|
|
2
|
+
mode: 'light' | 'dark' | 'system';
|
|
3
|
+
baseColor: 'neutral' | 'slate' | 'warm';
|
|
4
|
+
accentColor: string;
|
|
5
|
+
darkAccentColor?: string;
|
|
6
|
+
chartColors: string[];
|
|
7
|
+
density: 'compact' | 'comfortable' | 'spacious';
|
|
8
|
+
cornerRadius: 'none' | 'small' | 'medium' | 'large';
|
|
9
|
+
elevation: 'flat' | 'subtle' | 'raised';
|
|
10
|
+
typography: {
|
|
11
|
+
body: string;
|
|
12
|
+
heading: string;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type ThemeMode = AppearanceSettings['mode'];
|
|
16
|
+
export type ThemeController = {
|
|
17
|
+
getMode: () => ThemeMode;
|
|
18
|
+
setMode: (mode: ThemeMode) => void;
|
|
19
|
+
subscribe: (listener: () => void) => () => void;
|
|
20
|
+
};
|
|
21
|
+
export declare function parseAppearance(value: unknown): AppearanceSettings;
|
|
22
|
+
/**
|
|
23
|
+
* Install semantic tokens on the document root, including portaled UI. Returns a cleanup for
|
|
24
|
+
* system-theme listening.
|
|
25
|
+
*/
|
|
26
|
+
export declare function applyAppearance(value: unknown): () => void;
|
|
27
|
+
/** Viewer color mode persists independently of app-authored brand tokens. */
|
|
28
|
+
export declare function createThemeController(value: unknown): ThemeController;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** Data app delivery protocol. IDs identify a document, a shell session, and an individual call. */
|
|
2
|
+
export declare const BRIDGE = "altertable:data-app";
|
|
3
|
+
export declare const PARENT_PARAM = "__altertable_parent";
|
|
4
|
+
/** Bound pending requests in the client and host independently.
|
|
5
|
+
* The host enforces its own limit because frames can send messages directly. */
|
|
6
|
+
export declare const MAX_PENDING = 128;
|
|
7
|
+
export declare const REQUEST_TIMEOUT_MS = 60000;
|
|
8
|
+
export type TransportResponse = {
|
|
9
|
+
status: number;
|
|
10
|
+
body: unknown;
|
|
11
|
+
};
|
|
12
|
+
export type BridgeMessage = {
|
|
13
|
+
channel: typeof BRIDGE;
|
|
14
|
+
version: 1;
|
|
15
|
+
type: string;
|
|
16
|
+
documentId: string;
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
id?: string;
|
|
19
|
+
route?: string;
|
|
20
|
+
payload?: unknown;
|
|
21
|
+
response?: unknown;
|
|
22
|
+
code?: string;
|
|
23
|
+
message?: string;
|
|
24
|
+
/** Opaque host context, interpreted by optional app adapters. */
|
|
25
|
+
state?: unknown;
|
|
26
|
+
requestId?: string;
|
|
27
|
+
token?: string;
|
|
28
|
+
javascript?: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function isBridgeMessage(value: unknown): value is BridgeMessage;
|
|
31
|
+
export declare function validId(value: unknown): value is string;
|
|
32
|
+
export declare function validLocation(message: {
|
|
33
|
+
search?: unknown;
|
|
34
|
+
hash?: unknown;
|
|
35
|
+
}): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App identity and display scope shared by browser and server.
|
|
3
|
+
* @module @altertable/data-app/config
|
|
4
|
+
* @see https://github.com/altertable-ai/data-app/blob/main/docs/config.md
|
|
5
|
+
*/
|
|
6
|
+
export type DataAppConfig = {
|
|
7
|
+
title: string;
|
|
8
|
+
scope: {
|
|
9
|
+
organization: string;
|
|
10
|
+
environment: string;
|
|
11
|
+
};
|
|
12
|
+
appearance: unknown;
|
|
13
|
+
};
|
|
14
|
+
export declare function dataAppTitle(config: Pick<DataAppConfig, 'title' | 'scope'>): string;
|