@altertable/data-app 0.66.0 → 0.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +22 -16
- package/dist/chunks/{contract-19ckn3n7.js → index-23dktynd.js} +8 -6
- package/dist/chunks/index-23dktynd.js.map +10 -0
- package/dist/chunks/{contract-rfbakka4.js → index-ag0cgnvq.js} +169 -35
- package/dist/chunks/index-ag0cgnvq.js.map +13 -0
- package/dist/chunks/index-dq7b35fn.js +307 -0
- package/dist/chunks/index-dq7b35fn.js.map +13 -0
- package/dist/chunks/{contract-e11m4y7f.js → index-ev2b5aaf.js} +11 -6
- package/dist/chunks/{contract-e11m4y7f.js.map → index-ev2b5aaf.js.map} +3 -3
- package/dist/chunks/{contract-awa2d5b1.js → index-f5cnbrjz.js} +19 -2
- package/dist/chunks/{contract-awa2d5b1.js.map → index-f5cnbrjz.js.map} +4 -3
- package/dist/chunks/{contract-havnjmdr.js → index-gvers8jw.js} +2429 -1110
- package/dist/chunks/index-gvers8jw.js.map +109 -0
- package/dist/chunks/{contract-13j5zb3c.js → index-ktmvs916.js} +8 -7
- package/dist/chunks/index-ktmvs916.js.map +13 -0
- package/dist/chunks/index-mt93ff2b.js +28 -0
- package/dist/chunks/index-mt93ff2b.js.map +10 -0
- package/dist/chunks/{contract-tf8c3qpv.js → index-qd5xgx6y.js} +46 -5
- package/dist/chunks/index-qd5xgx6y.js.map +13 -0
- package/dist/chunks/{contract-pk603qj7.js → index-x69p7cv7.js} +12 -9
- package/dist/chunks/index-x69p7cv7.js.map +10 -0
- package/dist/client/index.js +6 -6
- package/dist/core/appearance.js +1 -1
- package/dist/core/config.js +7 -3
- package/dist/core/config.js.map +1 -1
- package/dist/core/contract.js +14 -6
- package/dist/core/contract.js.map +1 -1
- package/dist/core/format.js +1 -1
- package/dist/embed/index.js +12 -10
- package/dist/embed/index.js.map +4 -4
- package/dist/index.js +12 -0
- package/dist/index.js.map +9 -0
- package/dist/local.js +20 -30
- package/dist/local.js.map +7 -8
- package/dist/react/embed/index.js +1 -1
- package/dist/react/embed/index.js.map +2 -2
- package/dist/react/index.js +79 -29
- package/dist/react/index.js.map +5 -6
- package/dist/react/ui/index.js +1702 -177
- package/dist/react/ui/index.js.map +20 -9
- package/dist/server.js +13 -26
- package/dist/server.js.map +6 -7
- package/dist/types/client/annotations.d.ts +4 -0
- package/dist/types/client/data-client.d.ts +1 -2
- package/dist/types/client/iframe.d.ts +4 -2
- package/dist/types/core/annotations.d.ts +2 -0
- package/dist/types/core/config.d.ts +48 -4
- package/dist/types/core/contract.d.ts +20 -12
- package/dist/types/core/dimension.d.ts +6 -4
- package/dist/types/core/filters.d.ts +44 -0
- package/dist/types/core/messages.d.ts +2 -0
- package/dist/types/core/operation-types.d.ts +5 -2
- package/dist/types/core/operation.d.ts +3 -4
- package/dist/types/core/queries.d.ts +16 -0
- package/dist/types/core/uuid.d.ts +2 -0
- package/dist/types/core/variables.d.ts +32 -13
- package/dist/types/embed/runtime-html.d.ts +2 -0
- package/dist/types/embed/source.d.ts +2 -1
- package/dist/types/index.d.ts +3 -0
- package/dist/types/react/annotations/AnnotationBar.d.ts +7 -1
- package/dist/types/react/annotations/AnnotationEditor.d.ts +2 -1
- package/dist/types/react/annotations/useDataAppAnnotations.d.ts +3 -0
- package/dist/types/react/app-context.d.ts +3 -0
- package/dist/types/react/index.d.ts +3 -3
- package/dist/types/react/mount.d.ts +6 -4
- package/dist/types/react/style-contract.d.ts +34 -2
- package/dist/types/react/ui/Button.d.ts +3 -3
- package/dist/types/react/ui/ChartLegend.d.ts +31 -0
- package/dist/types/react/ui/Checkbox.d.ts +15 -4
- package/dist/types/react/ui/CheckboxGroup.d.ts +7 -0
- package/dist/types/react/ui/{Combobox.d.ts → ChoicePicker.d.ts} +10 -15
- package/dist/types/react/ui/ComposedChart.d.ts +41 -0
- package/dist/types/react/ui/ComposedChartLegend.d.ts +8 -0
- package/dist/types/react/ui/DataAppFrame.d.ts +1 -2
- package/dist/types/react/ui/DimensionPicker.d.ts +1 -1
- package/dist/types/react/ui/FilterActions.d.ts +10 -0
- package/dist/types/react/ui/FilterBar.d.ts +3 -0
- package/dist/types/react/ui/FunnelChart.d.ts +20 -0
- package/dist/types/react/ui/GettingStarted.d.ts +2 -4
- package/dist/types/react/ui/JourneyChart.d.ts +10 -0
- package/dist/types/react/ui/Menu.d.ts +12 -0
- package/dist/types/react/ui/NumberField.d.ts +27 -0
- package/dist/types/react/ui/NumberFilterPicker.d.ts +9 -0
- package/dist/types/react/ui/PieChart.d.ts +5 -2
- package/dist/types/react/ui/RadioGroup.d.ts +18 -0
- package/dist/types/react/ui/RetentionChart.d.ts +27 -0
- package/dist/types/react/ui/SearchInput.d.ts +2 -2
- package/dist/types/react/ui/Select.d.ts +14 -0
- package/dist/types/react/ui/SelectionMark.d.ts +2 -1
- package/dist/types/react/ui/TooltipSurface.d.ts +3 -0
- package/dist/types/react/ui/chart-data.d.ts +10 -1
- package/dist/types/react/ui/chart-primitives.d.ts +22 -0
- package/dist/types/react/ui/chartColor.d.ts +1 -0
- package/dist/types/react/ui/icons.d.ts +1 -0
- package/dist/types/react/ui/index.d.ts +33 -2
- package/dist/types/react/ui/journey-flow.d.ts +49 -0
- package/dist/types/react/ui/keyboard.d.ts +6 -0
- package/dist/types/react/ui/shortcuts.d.ts +7 -0
- package/dist/types/react/ui/useAppAppearance.d.ts +1 -1
- package/dist/types/react/ui/useSvgId.d.ts +2 -0
- package/dist/types/react/ui/variables.d.ts +4 -2
- package/dist/types/react/view-controls.d.ts +3 -1
- package/dist/types/server/handler.d.ts +4 -4
- package/dist/worker.js +21 -738
- package/docs/app-authoring.md +15 -1
- package/docs/client.md +8 -8
- package/docs/contract.md +43 -20
- package/docs/data-context.md +8 -6
- package/docs/embed.md +10 -2
- package/docs/formatting-and-appearance.md +23 -7
- package/docs/hosted-apps.md +9 -2
- package/docs/layout.md +1 -1
- package/docs/react-embed.md +3 -2
- package/docs/react.md +4 -3
- package/docs/server-bun.md +1 -1
- package/docs/server.md +4 -4
- package/docs/styling.md +10 -3
- package/docs/ui.md +176 -3
- package/docs/variables.md +80 -7
- package/docs/views.md +4 -8
- package/docs/widgets.md +19 -13
- package/docs/worker.md +2 -1
- package/examples/starter-data-app/index.tsx +34 -32
- package/package.json +16 -11
- package/dist/chunks/contract-13j5zb3c.js.map +0 -13
- package/dist/chunks/contract-19ckn3n7.js.map +0 -10
- package/dist/chunks/contract-h8a6f559.js +0 -286
- package/dist/chunks/contract-h8a6f559.js.map +0 -12
- package/dist/chunks/contract-havnjmdr.js.map +0 -99
- package/dist/chunks/contract-pk603qj7.js.map +0 -10
- package/dist/chunks/contract-rfbakka4.js.map +0 -13
- package/dist/chunks/contract-tf8c3qpv.js.map +0 -11
- package/dist/chunks/contract-wz59z8pq.js +0 -8
- package/dist/chunks/contract-wz59z8pq.js.map +0 -10
- /package/dist/chunks/{contract-m6n8ctfc.js → index-m6n8ctfc.js} +0 -0
- /package/dist/chunks/{contract-m6n8ctfc.js.map → index-m6n8ctfc.js.map} +0 -0
- /package/dist/chunks/{contract-mev09s5v.js → index-mev09s5v.js} +0 -0
- /package/dist/chunks/{contract-mev09s5v.js.map → index-mev09s5v.js.map} +0 -0
package/dist/server.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/core/contract.ts
|
|
3
3
|
import { parseDate, today } from "@internationalized/date";
|
|
4
|
-
|
|
5
|
-
// src/core/invariant.ts
|
|
6
|
-
function invariant(condition, message) {
|
|
7
|
-
if (!condition)
|
|
8
|
-
throw new Error(`Invariant failed: ${message}`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// src/core/contract.ts
|
|
12
|
-
function defineQueryNames(names) {
|
|
13
|
-
const values = Object.values(names);
|
|
14
|
-
invariant(values.every((name) => !!name.trim()) && new Set(values).size === values.length, "Query names must be nonempty and unique.");
|
|
15
|
-
return names;
|
|
16
|
-
}
|
|
17
4
|
var sourceErrorBrand = Symbol.for("@altertable/data-app/DataSourceError");
|
|
18
5
|
|
|
19
6
|
class DataSourceError extends Error {
|
|
@@ -31,17 +18,14 @@ class DataSourceError extends Error {
|
|
|
31
18
|
this.name = "DataSourceError";
|
|
32
19
|
}
|
|
33
20
|
}
|
|
34
|
-
var connectionQueryNames = defineQueryNames({
|
|
35
|
-
connection: "connection-check"
|
|
36
|
-
});
|
|
37
21
|
|
|
38
22
|
// src/core/operation.ts
|
|
39
23
|
async function executeDataOperation(operation, value, {
|
|
40
24
|
operationName,
|
|
41
25
|
lakehouse,
|
|
42
26
|
signal: requestSignal,
|
|
43
|
-
|
|
44
|
-
|
|
27
|
+
requestId,
|
|
28
|
+
queryParams
|
|
45
29
|
}) {
|
|
46
30
|
requestSignal.throwIfAborted();
|
|
47
31
|
let input;
|
|
@@ -58,14 +42,15 @@ async function executeDataOperation(operation, value, {
|
|
|
58
42
|
try {
|
|
59
43
|
const output = await runOperation(operation, input, {
|
|
60
44
|
lakehouse: execution.lakehouse,
|
|
61
|
-
signal
|
|
45
|
+
signal,
|
|
46
|
+
queryParams
|
|
62
47
|
});
|
|
63
48
|
const body = {
|
|
64
49
|
data: operation.output(output),
|
|
65
50
|
requestId,
|
|
66
51
|
queriedAt: new Date().toISOString(),
|
|
67
52
|
queryIds: execution.queryIds,
|
|
68
|
-
|
|
53
|
+
queries: execution.queries
|
|
69
54
|
};
|
|
70
55
|
const serializedBody = JSON.stringify(body);
|
|
71
56
|
const responseBytes = new TextEncoder().encode(serializedBody).byteLength;
|
|
@@ -89,7 +74,8 @@ function createOperationLakehouse(operation, source, signal, operationName) {
|
|
|
89
74
|
throw new Error(`Query name is not registered for operation ${operationName}.`);
|
|
90
75
|
const query = {
|
|
91
76
|
name: options.name ?? `Query ${queries.length + 1}`,
|
|
92
|
-
statement
|
|
77
|
+
statement,
|
|
78
|
+
...options.params === undefined ? {} : { params: options.params }
|
|
93
79
|
};
|
|
94
80
|
queries.push(query);
|
|
95
81
|
const limit = Math.min(options.limit, operation.policy.maxQueryRows);
|
|
@@ -97,6 +83,7 @@ function createOperationLakehouse(operation, source, signal, operationName) {
|
|
|
97
83
|
try {
|
|
98
84
|
result = await source.queryAll(statement, {
|
|
99
85
|
limit,
|
|
86
|
+
...options.params === undefined ? {} : { params: options.params },
|
|
100
87
|
signal: AbortSignal.any([signal, options.signal])
|
|
101
88
|
});
|
|
102
89
|
} catch (error) {
|
|
@@ -115,7 +102,7 @@ function createOperationLakehouse(operation, source, signal, operationName) {
|
|
|
115
102
|
};
|
|
116
103
|
return { lakehouse, queries, queryIds };
|
|
117
104
|
}
|
|
118
|
-
async function runOperation(operation, input, { lakehouse, signal }) {
|
|
105
|
+
async function runOperation(operation, input, { lakehouse, signal, queryParams }) {
|
|
119
106
|
let abort;
|
|
120
107
|
try {
|
|
121
108
|
signal.throwIfAborted();
|
|
@@ -132,7 +119,7 @@ async function runOperation(operation, input, { lakehouse, signal }) {
|
|
|
132
119
|
cancelled,
|
|
133
120
|
Promise.resolve().then(() => {
|
|
134
121
|
signal.throwIfAborted();
|
|
135
|
-
return operation.run({ lakehouse, signal }, input);
|
|
122
|
+
return operation.run({ lakehouse, signal, queryParams }, input);
|
|
136
123
|
})
|
|
137
124
|
]);
|
|
138
125
|
} finally {
|
|
@@ -223,10 +210,10 @@ function createDataHandler(operations, authorize) {
|
|
|
223
210
|
try {
|
|
224
211
|
const { serializedBody } = await executeDataOperation(operation, input, {
|
|
225
212
|
lakehouse: access.lakehouse,
|
|
226
|
-
includeSql: access.canDiscloseSql,
|
|
227
213
|
signal,
|
|
228
214
|
requestId,
|
|
229
|
-
operationName: name
|
|
215
|
+
operationName: name,
|
|
216
|
+
queryParams: access.queryParams
|
|
230
217
|
});
|
|
231
218
|
return new Response(serializedBody, {
|
|
232
219
|
headers: {
|
|
@@ -300,4 +287,4 @@ export {
|
|
|
300
287
|
createDataHandler
|
|
301
288
|
};
|
|
302
289
|
|
|
303
|
-
//# debugId=
|
|
290
|
+
//# debugId=A8B5257A307F4C0764756E2164756E21
|
package/dist/server.js.map
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/core/contract.ts", "../src/core/
|
|
3
|
+
"sources": ["../src/core/contract.ts", "../src/core/operation.ts", "../src/server/handler.ts"],
|
|
4
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';\nimport type { DateRange as DateRangeInput } from '@/src/core/date-range';\nimport type {\n QueryResult,\n DataOperation,\n OperationContext,\n} from '@/src/core/operation-types';\nexport type {\n QueryResult,\n DisclosedQuery,\n Lakehouse,\n OperationContext,\n DataOperation,\n DataOperations,\n} from '@/src/core/operation-types';\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 { DateRange as DateRangeInput } from '@/src/core/date-range';\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\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\nconst sourceErrorBrand = Symbol.for('@altertable/data-app/DataSourceError');\n\nexport class DataSourceError extends Error {\n readonly [sourceErrorBrand] = true;\n queryName?: string;\n\n static [Symbol.hasInstance](value: unknown): boolean {\n return (\n value instanceof Error &&\n (value as { [sourceErrorBrand]?: unknown })[sourceErrorBrand] === true\n );\n }\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\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 {\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 {\n createMessageRouter,\n defineMessageRoute,\n defineDataQueryRoute,\n MessageRoutingError,\n dataAppRoutes,\n navigationUpdateRoute,\n sqlQueryRoute,\n} from '@/src/core/messages';\nexport type {\n MessageContext,\n RoutedMessage,\n MessageDispatcher,\n MessageTransport,\n MessageRoute,\n MessageRoutes,\n MessageInput,\n MessageHandlers,\n MessageOutput,\n OperationContracts,\n DataQueryInput,\n DataQueryBody,\n DataQueryRoute,\n NavigationUpdate,\n SqlQueryInput,\n} from '@/src/core/messages';\nexport type { TransportResponse } from '@/src/core/bridge';\n\nexport {\n annotationDraftRoute,\n annotationModeRoute,\n annotationEditorStateRoute,\n annotationUpdateRoute,\n parseDataAppAnnotationDraft,\n} from '@/src/core/annotations';\nexport type {\n DataAppAnnotationDraft,\n DataAppAnnotationPresentation,\n} from '@/src/core/annotations';\n",
|
|
6
|
-
"
|
|
7
|
-
"import {
|
|
8
|
-
"import {\n executeDataOperation,\n toDataOperationFailure,\n} from '@/src/core/operation';\nimport {\n DataSourceError,\n type DataOperations,\n type Lakehouse,\n} from '@/src/core/contract';\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 = 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 signal = request.signal;\n try {\n const { serializedBody } = await executeDataOperation(operation, input, {\n lakehouse: access.lakehouse,\n includeSql: access.canDiscloseSql,\n signal,\n requestId,\n operationName: name,\n });\n return new Response(serializedBody, {\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 const failure = toDataOperationFailure(error, signal.aborted);\n\n return problem(failure.status, failure.code, failure.message, requestId);\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"
|
|
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';\nimport {\n snapshotQueries,\n resolveQueryParameters,\n type QueryDefinitions,\n type QueryParameters,\n type QueryParameterValue,\n} from '@/src/core/queries';\nexport type {\n QueryDefinitions,\n QueryParameters,\n QueryParameterValue,\n} from '@/src/core/queries';\nimport type { DateRange as DateRangeInput } from '@/src/core/date-range';\nimport type {\n QueryResult,\n DataOperation,\n OperationContext,\n} from '@/src/core/operation-types';\nexport type {\n QueryResult,\n DisclosedQuery,\n Lakehouse,\n OperationContext,\n DataOperation,\n DataOperations,\n} from '@/src/core/operation-types';\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 { DateRange as DateRangeInput } from '@/src/core/date-range';\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\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\nconst sourceErrorBrand = Symbol.for('@altertable/data-app/DataSourceError');\n\nexport class DataSourceError extends Error {\n readonly [sourceErrorBrand] = true;\n queryName?: string;\n\n static [Symbol.hasInstance](value: unknown): boolean {\n return (\n value instanceof Error &&\n (value as { [sourceErrorBrand]?: unknown })[sourceErrorBrand] === true\n );\n }\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\ntype RegisteredQueryParameters<\n Queries extends QueryDefinitions,\n Name extends keyof Queries,\n> = keyof Queries[Name]['params'] extends never\n ? Record<string, never>\n : Partial<Record<keyof Queries[Name]['params'], QueryParameterValue>>;\n\nexport type RegisteredQuery<Queries extends QueryDefinitions> = <\n Name extends keyof Queries & string,\n const Params extends object = {},\n>(\n name: Name,\n params?: Params &\n NoInfer<RegisteredQueryParameters<Queries, Name>> &\n NoInfer<\n Record<Exclude<keyof Params, keyof Queries[Name]['params']>, never>\n > &\n NoInfer<Params extends (...args: never[]) => unknown ? never : object>,\n options?: { limit?: number }\n) => Promise<QueryResult>;\n\nexport function defineOperation<\n Input,\n Output,\n const Queries extends QueryDefinitions,\n>(\n operation: Omit<DataOperation<Input, Output>, 'run' | 'queryNames'> & {\n queries: Queries;\n run: (\n context: Pick<OperationContext, 'signal'> & {\n query: RegisteredQuery<NoInfer<Queries>>;\n },\n input: Input\n ) => Promise<Output>;\n }\n): Omit<DataOperation<Input, Output>, 'queryNames'> & {\n queryNames: { readonly [Name in keyof Queries & string]: Name };\n} {\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 const queries = snapshotQueries(operation.queries);\n const queryNames = Object.fromEntries(\n Object.keys(queries).map(name => [name, name])\n ) as { readonly [Name in keyof Queries & string]: Name };\n for (const input of operation.checks) operation.input(input);\n\n return {\n ...operation,\n queryNames,\n run(context, input) {\n function query(\n name: keyof Queries & string,\n values: Partial<QueryParameters> = {},\n options?: { limit?: number }\n ) {\n invariant(Object.hasOwn(queries, name), `Unknown query name: ${name}.`);\n const definition = queries[name]!;\n const params = resolveQueryParameters(\n definition.params,\n values,\n context.queryParams ?? {}\n );\n return context.lakehouse.queryAll(definition.statement, {\n name,\n params,\n limit: options?.limit ?? operation.policy.maxQueryRows,\n signal: context.signal,\n });\n }\n return operation.run({ signal: context.signal, query }, input);\n },\n };\n}\n\n/** A connectivity probe uses the app's registered connection query. */\nexport function connectionCheck<\n const Queries extends QueryDefinitions & {\n connection: QueryDefinitions[string];\n },\n>(queries: Queries) {\n return defineOperation({\n queries,\n input: parseEmptyInput,\n output: parseTrue,\n checks: [{}],\n policy: { maxQueryRows: 1, maxDurationMs: 15_000 },\n async run({ query }): Promise<true> {\n await query('connection');\n return true;\n },\n });\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 {\n createMessageRouter,\n defineMessageRoute,\n defineDataQueryRoute,\n MessageRoutingError,\n dataAppRoutes,\n navigationUpdateRoute,\n sqlQueryRoute,\n} from '@/src/core/messages';\nexport type {\n MessageContext,\n RoutedMessage,\n MessageDispatcher,\n MessageTransport,\n MessageRoute,\n MessageRoutes,\n MessageInput,\n MessageHandlers,\n MessageOutput,\n OperationContracts,\n DataQueryInput,\n DataQueryBody,\n DataQueryRoute,\n NavigationUpdate,\n SqlQueryInput,\n} from '@/src/core/messages';\nexport type { TransportResponse } from '@/src/core/bridge';\n\nexport {\n annotationDraftRoute,\n annotationModeRoute,\n annotationEditorStateRoute,\n annotationSendRoute,\n annotationUpdateRoute,\n parseDataAppAnnotationDraft,\n} from '@/src/core/annotations';\nexport type {\n DataAppAnnotationDraft,\n DataAppAnnotationPresentation,\n} from '@/src/core/annotations';\n\nexport {\n numberFilter,\n booleanFilter,\n parseNumberSelection,\n parseBooleanSelection,\n} from '@/src/core/filters';\nexport type {\n NumberSelection,\n NumberOperator,\n NumberFilter,\n BooleanSelection,\n BooleanFilter,\n} from '@/src/core/filters';\n",
|
|
6
|
+
"import { DataSourceError } from '@/src/core/contract';\nimport type {\n DataOperations,\n DisclosedQuery,\n Lakehouse,\n OperationContext,\n DataQueryBody,\n} from '@/src/core/operation-types';\n\ntype Operation = DataOperations[string];\ntype OperationExecutionOptions = {\n operationName: string;\n lakehouse: Lakehouse;\n signal: AbortSignal;\n requestId: string;\n queryParams?: OperationContext['queryParams'];\n};\n\n/** Shared operation execution. Adapters own authorization, delivery, and public errors. */\nexport async function executeDataOperation(\n operation: Operation,\n value: unknown,\n {\n operationName,\n lakehouse,\n signal: requestSignal,\n requestId,\n queryParams,\n }: OperationExecutionOptions\n) {\n requestSignal.throwIfAborted();\n let input: unknown;\n try {\n input = operation.input(value);\n } catch {\n throw new OperationInputError();\n }\n\n const signal = AbortSignal.any([\n requestSignal,\n AbortSignal.timeout(operation.policy.maxDurationMs),\n ]);\n const execution = createOperationLakehouse(\n operation,\n lakehouse,\n signal,\n operationName\n );\n try {\n const output = await runOperation(operation, input, {\n lakehouse: execution.lakehouse,\n signal,\n queryParams,\n });\n const body: DataQueryBody<unknown> = {\n data: operation.output(output),\n requestId,\n queriedAt: new Date().toISOString(),\n queryIds: execution.queryIds,\n queries: execution.queries,\n };\n const serializedBody = JSON.stringify(body);\n const responseBytes = new TextEncoder().encode(serializedBody).byteLength;\n if (responseBytes > (operation.policy.maxResponseBytes ?? 1_000_000))\n throw new Error('Response exceeds byte limit.');\n\n return { body, serializedBody };\n } catch (error) {\n if (signal.aborted && !(error instanceof DataSourceError))\n throw new OperationTimeoutError();\n throw error;\n }\n}\n\n/** Query evidence follows statement invocation order; query IDs retain completion order. */\nfunction createOperationLakehouse(\n operation: Operation,\n source: Lakehouse,\n signal: AbortSignal,\n operationName: string\n) {\n const queryIds: string[] = [];\n const queries: DisclosedQuery[] = [];\n const lakehouse: 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 ${operationName}.`\n );\n const query: DisclosedQuery = {\n name: options.name ?? `Query ${queries.length + 1}`,\n statement,\n ...(options.params === undefined ? {} : { params: options.params }),\n };\n queries.push(query);\n const limit = Math.min(options.limit, operation.policy.maxQueryRows);\n let result;\n try {\n result = await source.queryAll(statement, {\n limit,\n ...(options.params === undefined ? {} : { params: options.params }),\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 > limit)\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\n return { lakehouse, queries, queryIds };\n}\n\n/** Settle cancellation even when operation code or its lakehouse ignores the signal. */\nasync function runOperation(\n operation: Operation,\n input: unknown,\n { lakehouse, signal, queryParams }: OperationContext\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\n return await Promise.race([\n cancelled,\n Promise.resolve().then(() => {\n signal.throwIfAborted();\n\n return operation.run(\n { lakehouse, signal, queryParams },\n input as never\n );\n }),\n ]);\n } finally {\n if (abort) signal.removeEventListener('abort', abort);\n }\n}\n\nclass OperationTimeoutError extends Error {}\n\nclass OperationInputError extends Error {}\n\nconst sourceErrorMessages = {\n unauthorized:\n 'This app could not authenticate its data connection. Contact the app owner.',\n forbidden:\n 'This app cannot access the requested data. Contact the app owner.',\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\nexport function toDataOperationFailure(error: unknown, aborted = false) {\n if (error instanceof OperationInputError)\n return {\n status: 400,\n code: 'invalid_input',\n message: \"Check this operation's inputs.\",\n };\n if (error instanceof DataSourceError)\n return {\n status: error.reason === 'rate_limited' ? 429 : 502,\n code: `source_${error.reason}`,\n message: sourceErrorMessages[error.reason],\n };\n const timedOut = aborted || error instanceof OperationTimeoutError;\n return {\n status: timedOut ? 504 : 502,\n code: timedOut ? 'timeout' : 'query_failed',\n message: timedOut\n ? 'The data request timed out.'\n : 'The data request failed.',\n };\n}\n",
|
|
7
|
+
"import {\n executeDataOperation,\n toDataOperationFailure,\n} from '@/src/core/operation';\nimport {\n DataSourceError,\n type DataOperations,\n type Lakehouse,\n type QueryParameters,\n} from '@/src/core/contract';\n\n/** Authorization result for one viewer request. A hosted adapter must scope its lakehouse client. */\nexport type RequestAccess = {\n lakehouse: Lakehouse;\n /** Trusted values shared across queries; each query uses only its declared keys. */\n queryParams?: QueryParameters;\n};\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. Query evidence accompanies every result.\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 = 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 signal = request.signal;\n try {\n const { serializedBody } = await executeDataOperation(operation, input, {\n lakehouse: access.lakehouse,\n signal,\n requestId,\n operationName: name,\n queryParams: access.queryParams,\n });\n return new Response(serializedBody, {\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 const failure = toDataOperationFailure(error, signal.aborted);\n\n return problem(failure.status, failure.code, failure.message, requestId);\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"
|
|
9
8
|
],
|
|
10
|
-
"mappings": ";;AAKA
|
|
11
|
-
"debugId": "
|
|
9
|
+
"mappings": ";;AAKA;AA+RA,IAAM,mBAAmB,OAAO,IAAI,sCAAsC;AAAA;AAEnE,MAAM,wBAAwB,MAAM;AAAA,EAYvB;AAAA,EAMA;AAAA,GAjBR,oBAAoB;AAAA,EAC9B;AAAA,UAEQ,OAAO,YAAY,CAAC,OAAyB;AAAA,IACnD,OACE,iBAAiB,SAChB,MAA2C,sBAAsB;AAAA;AAAA,EAItE,WAAW,CACO,QAMA,QAChB;AAAA,IACA,MAAM,eAAe,SAAS,SAAS,KAAK,YAAY,KAAK;AAAA,IAR7C;AAAA,IAMA;AAAA,IAGhB,KAAK,OAAO;AAAA;AAEhB;;;AC1SA,eAAsB,oBAAoB,CACxC,WACA;AAAA,EAEE;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA;AAAA,GAEF;AAAA,EACA,cAAc,eAAe;AAAA,EAC7B,IAAI;AAAA,EACJ,IAAI;AAAA,IACF,QAAQ,UAAU,MAAM,KAAK;AAAA,IAC7B,MAAM;AAAA,IACN,MAAM,IAAI;AAAA;AAAA,EAGZ,MAAM,SAAS,YAAY,IAAI;AAAA,IAC7B;AAAA,IACA,YAAY,QAAQ,UAAU,OAAO,aAAa;AAAA,EACpD,CAAC;AAAA,EACD,MAAM,YAAY,yBAChB,WACA,WACA,QACA,aACF;AAAA,EACA,IAAI;AAAA,IACF,MAAM,SAAS,MAAM,aAAa,WAAW,OAAO;AAAA,MAClD,WAAW,UAAU;AAAA,MACrB;AAAA,MACA;AAAA,IACF,CAAC;AAAA,IACD,MAAM,OAA+B;AAAA,MACnC,MAAM,UAAU,OAAO,MAAM;AAAA,MAC7B;AAAA,MACA,WAAW,IAAI,KAAK,EAAE,YAAY;AAAA,MAClC,UAAU,UAAU;AAAA,MACpB,SAAS,UAAU;AAAA,IACrB;AAAA,IACA,MAAM,iBAAiB,KAAK,UAAU,IAAI;AAAA,IAC1C,MAAM,gBAAgB,IAAI,YAAY,EAAE,OAAO,cAAc,EAAE;AAAA,IAC/D,IAAI,iBAAiB,UAAU,OAAO,oBAAoB;AAAA,MACxD,MAAM,IAAI,MAAM,8BAA8B;AAAA,IAEhD,OAAO,EAAE,MAAM,eAAe;AAAA,IAC9B,OAAO,OAAO;AAAA,IACd,IAAI,OAAO,WAAW,EAAE,iBAAiB;AAAA,MACvC,MAAM,IAAI;AAAA,IACZ,MAAM;AAAA;AAAA;AAKV,SAAS,wBAAwB,CAC/B,WACA,QACA,QACA,eACA;AAAA,EACA,MAAM,WAAqB,CAAC;AAAA,EAC5B,MAAM,UAA4B,CAAC;AAAA,EACnC,MAAM,YAAuB;AAAA,SACrB,SAAQ,CAAC,WAAW,SAAS;AAAA,MACjC,IAAI,CAAC,OAAO,UAAU,QAAQ,KAAK,KAAK,QAAQ,QAAQ;AAAA,QACtD,MAAM,IAAI,MAAM,mCAAmC;AAAA,MACrD,IACE,UAAU,cACV,CAAC,OAAO,OAAO,UAAU,UAAU,EAAE,SAAS,QAAQ,QAAQ,EAAE;AAAA,QAEhE,MAAM,IAAI,MACR,8CAA8C,gBAChD;AAAA,MACF,MAAM,QAAwB;AAAA,QAC5B,MAAM,QAAQ,QAAQ,SAAS,QAAQ,SAAS;AAAA,QAChD;AAAA,WACI,QAAQ,WAAW,YAAY,CAAC,IAAI,EAAE,QAAQ,QAAQ,OAAO;AAAA,MACnE;AAAA,MACA,QAAQ,KAAK,KAAK;AAAA,MAClB,MAAM,QAAQ,KAAK,IAAI,QAAQ,OAAO,UAAU,OAAO,YAAY;AAAA,MACnE,IAAI;AAAA,MACJ,IAAI;AAAA,QACF,SAAS,MAAM,OAAO,SAAS,WAAW;AAAA,UACxC;AAAA,aACI,QAAQ,WAAW,YAAY,CAAC,IAAI,EAAE,QAAQ,QAAQ,OAAO;AAAA,UACjE,QAAQ,YAAY,IAAI,CAAC,QAAQ,QAAQ,MAAM,CAAC;AAAA,QAClD,CAAC;AAAA,QACD,OAAO,OAAO;AAAA,QACd,IAAI,iBAAiB;AAAA,UAAiB,MAAM,YAAY,MAAM;AAAA,QAC9D,MAAM;AAAA;AAAA,MAER,IAAI,OAAO,KAAK,SAAS;AAAA,QACvB,MAAM,IAAI,MAAM,2BAA2B;AAAA,MAC7C,IAAI,OAAO,SAAS;AAAA,QAClB,MAAM,UAAU,OAAO;AAAA,QACvB,SAAS,KAAK,OAAO,OAAO;AAAA,MAC9B;AAAA,MAEA,OAAO;AAAA;AAAA,EAEX;AAAA,EAEA,OAAO,EAAE,WAAW,SAAS,SAAS;AAAA;AAIxC,eAAe,YAAY,CACzB,WACA,SACE,WAAW,QAAQ,eACrB;AAAA,EACA,IAAI;AAAA,EACJ,IAAI;AAAA,IACF,OAAO,eAAe;AAAA,IACtB,MAAM,YAAY,IAAI,QAAe,CAAC,GAAG,WAAW;AAAA,MAClD,SAAS,aAAa,GAAG;AAAA,QACvB,OAAO,IAAI,MAAM,kBAAkB,CAAC;AAAA;AAAA,MAEtC,QAAQ;AAAA,MACR,OAAO,iBAAiB,SAAS,eAAe,EAAE,MAAM,KAAK,CAAC;AAAA,MAC9D,IAAI,OAAO;AAAA,QAAS,cAAc;AAAA,KACnC;AAAA,IAED,OAAO,MAAM,QAAQ,KAAK;AAAA,MACxB;AAAA,MACA,QAAQ,QAAQ,EAAE,KAAK,MAAM;AAAA,QAC3B,OAAO,eAAe;AAAA,QAEtB,OAAO,UAAU,IACf,EAAE,WAAW,QAAQ,YAAY,GACjC,KACF;AAAA,OACD;AAAA,IACH,CAAC;AAAA,YACD;AAAA,IACA,IAAI;AAAA,MAAO,OAAO,oBAAoB,SAAS,KAAK;AAAA;AAAA;AAAA;AAIxD,MAAM,8BAA8B,MAAM;AAAC;AAAA;AAE3C,MAAM,4BAA4B,MAAM;AAAC;AAEzC,IAAM,sBAAsB;AAAA,EAC1B,cACE;AAAA,EACF,WACE;AAAA,EACF,cAAc;AAAA,EACd,gBACE;AAAA,EACF,aAAa;AACf;AAEO,SAAS,sBAAsB,CAAC,OAAgB,UAAU,OAAO;AAAA,EACtE,IAAI,iBAAiB;AAAA,IACnB,OAAO;AAAA,MACL,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,SAAS;AAAA,IACX;AAAA,EACF,IAAI,iBAAiB;AAAA,IACnB,OAAO;AAAA,MACL,QAAQ,MAAM,WAAW,iBAAiB,MAAM;AAAA,MAChD,MAAM,UAAU,MAAM;AAAA,MACtB,SAAS,oBAAoB,MAAM;AAAA,IACrC;AAAA,EACF,MAAM,WAAW,WAAW,iBAAiB;AAAA,EAC7C,OAAO;AAAA,IACL,QAAQ,WAAW,MAAM;AAAA,IACzB,MAAM,WAAW,YAAY;AAAA,IAC7B,SAAS,WACL,gCACA;AAAA,EACN;AAAA;;;AC5KK,SAAS,iBAAiB,CAC/B,YACA,WACA;AAAA,EACA,eAAe,iBAAiB,CAAC,SAAqC;AAAA,IACpE,MAAM,YAAY,OAAO,WAAW;AAAA,IACpC,MAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAAA,IAC/B,IAAI,CAAC,IAAI,SAAS,WAAW,YAAY;AAAA,MACvC,OAAO,IAAI,SAAS,aAAa,EAAE,QAAQ,IAAI,CAAC;AAAA,IAClD,MAAM,cAAc,IAAI,SAAS,MAAM,aAAa,MAAM;AAAA,IAC1D,IAAI,CAAC,eAAe,YAAY,SAAS,GAAG;AAAA,MAC1C,OAAO,QAAQ,KAAK,aAAa,2BAA2B,SAAS;AAAA,IACvE,IAAI;AAAA,IACJ,IAAI;AAAA,MACF,OAAO,mBAAmB,WAAW;AAAA,MACrC,MAAM;AAAA,MACN,OAAO,QAAQ,KAAK,aAAa,2BAA2B,SAAS;AAAA;AAAA,IAEvE,IAAI,CAAC,OAAO,OAAO,YAAY,IAAI;AAAA,MACjC,OAAO,QAAQ,KAAK,aAAa,2BAA2B,SAAS;AAAA,IACvE,IAAI,QAAQ,WAAW;AAAA,MACrB,OAAO,QAAQ,KAAK,sBAAsB,aAAa,SAAS;AAAA,IAClE,IACE,QAAQ,QACL,IAAI,cAAc,GACjB,MAAM,GAAG,EAAE,IACX,KAAK,EACN,YAAY,MAAM;AAAA,MAErB,OAAO,QACL,KACA,0BACA,yBACA,SACF;AAAA,IACF,MAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ;AAAA,IAC3C,MAAM,YAAY,QAAQ,QAAQ,IAAI,gBAAgB;AAAA,IACtD,MAAM,YAAY,QAAQ,QAAQ,IAAI,gBAAgB;AAAA,IACtD,IACG,WAAW,QAAQ,WAAW,IAAI,UAClC,cAAc,QAAQ,cAAc,iBACpC,cAAc,QAAQ,WAAW,QACjC,cAAc,QACb,cAAc,UACd,cAAc;AAAA,MAEhB,OAAO,QACL,KACA,wBACA,6CACA,SACF;AAAA,IACF,IAAI;AAAA,IACJ,IAAI;AAAA,MACF,SAAS,MAAM,UAAU,SAAS,IAAI;AAAA,MACtC,MAAM;AAAA,MACN,OAAO,QACL,KACA,aACA,uCACA,SACF;AAAA;AAAA,IAEF,MAAM,YAAY,WAAW;AAAA,IAC7B,IAAI;AAAA,IACJ,IAAI,QAAQ,OAAO;AAAA,MACjB,OAAO,QAAQ,KAAK,WAAW,+BAA+B,SAAS;AAAA,IACzE,IAAI;AAAA,MACF,MAAM,OAAO,MAAM,UAAU,OAAO;AAAA,MACpC,QAAQ,KAAK,MAAM,IAAI;AAAA,MACvB,OAAO,OAAO;AAAA,MACd,IAAI,iBAAiB;AAAA,QACnB,OAAO,QACL,KACA,mBACA,uBACA,SACF;AAAA,MAEF,OAAO,QACL,KACA,iBACA,kCACA,SACF;AAAA;AAAA,IAEF,MAAM,SAAS,QAAQ;AAAA,IACvB,IAAI;AAAA,MACF,QAAQ,mBAAmB,MAAM,qBAAqB,WAAW,OAAO;AAAA,QACtE,WAAW,OAAO;AAAA,QAClB;AAAA,QACA;AAAA,QACA,eAAe;AAAA,QACf,aAAa,OAAO;AAAA,MACtB,CAAC;AAAA,MACD,OAAO,IAAI,SAAS,gBAAgB;AAAA,QAClC,SAAS;AAAA,UACP,iBAAiB;AAAA,UACjB,gBAAgB;AAAA,QAClB;AAAA,MACF,CAAC;AAAA,MACD,OAAO,OAAO;AAAA,MACd,MAAM,SAAS,iBAAiB,kBAAkB,QAAQ;AAAA,MAC1D,QAAQ,MACN,kBAAkB,gBAAgB,eAAe,SAAS,GAAG,OAAO,SAAS,OAAO,SAAS,KAAK,OAAO,YAAY,KAAK,OAAO,YAAY,OAAO,OAAO,cAAc,OAAO,iBAAiB,QAAQ,MAAM,OAAO,WACxN;AAAA,MACA,MAAM,UAAU,uBAAuB,OAAO,OAAO,OAAO;AAAA,MAE5D,OAAO,QAAQ,QAAQ,QAAQ,QAAQ,MAAM,QAAQ,SAAS,SAAS;AAAA;AAAA;AAAA,EAI3E,OAAO;AAAA;AAGT,SAAS,OAAO,CACd,QACA,MACA,SACA,WACU;AAAA,EACV,OAAO,SAAS,KACd,EAAE,OAAO,EAAE,MAAM,SAAS,UAAU,EAAE,GACtC,EAAE,QAAQ,SAAS,EAAE,iBAAiB,WAAW,EAAE,CACrD;AAAA;AAGF,IAAM,kBAAkB;AAAA;AACxB,MAAM,2BAA2B,MAAM;AAAC;AAGxC,eAAe,SAAS,CAAC,SAAmC;AAAA,EAC1D,MAAM,SAAS,QAAQ,MAAM,UAAU;AAAA,EACvC,IAAI,CAAC;AAAA,IAAQ,OAAO;AAAA,EACpB,MAAM,SAAuB,CAAC;AAAA,EAC9B,IAAI,OAAO;AAAA,EACX,IAAI;AAAA,EAEJ,SAAS,YAAY,GAAG;AAAA,IAEjB,OAAQ,OAAO,EAAE,MAAM,MAAM,EAAE;AAAA;AAAA,EAGtC,IAAI;AAAA,IACF,QAAQ,OAAO,eAAe;AAAA,IAC9B,MAAM,YAAY,IAAI,QAAe,CAAC,GAAG,WAAW;AAAA,MAClD,SAAS,aAAa,GAAG;AAAA,QACvB,OAAO,IAAI,MAAM,kBAAkB,CAAC;AAAA,QACpC,aAAa;AAAA;AAAA,MAEf,QAAQ;AAAA,MACR,QAAQ,OAAO,iBAAiB,SAAS,eAAe,EAAE,MAAM,KAAK,CAAC;AAAA,MACtE,IAAI,QAAQ,OAAO;AAAA,QAAS,cAAc;AAAA,KAC3C;AAAA,IACD,OAAO,MAAM;AAAA,MACX,QAAQ,MAAM,UAAU,MAAM,QAAQ,KAAK,CAAC,OAAO,KAAK,GAAG,SAAS,CAAC;AAAA,MACrE,IAAI;AAAA,QAAM;AAAA,MACV,QAAQ,MAAM;AAAA,MACd,IAAI,OAAO,iBAAiB;AAAA,QAC1B,aAAa;AAAA,QACb,MAAM,IAAI;AAAA,MACZ;AAAA,MACA,OAAO,KAAK,KAAK;AAAA,IACnB;AAAA,IACA,MAAM,QAAQ,IAAI,WAAW,IAAI;AAAA,IACjC,IAAI,SAAS;AAAA,IACb,WAAW,SAAS,QAAQ;AAAA,MAC1B,MAAM,IAAI,OAAO,MAAM;AAAA,MACvB,UAAU,MAAM;AAAA,IAClB;AAAA,IAEA,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AAAA,YACrC;AAAA,IACA,IAAI;AAAA,MAAO,QAAQ,OAAO,oBAAoB,SAAS,KAAK;AAAA,IAC5D,OAAO,YAAY;AAAA;AAAA;",
|
|
10
|
+
"debugId": "A8B5257A307F4C0764756E2164756E21",
|
|
12
11
|
"names": []
|
|
13
12
|
}
|
|
@@ -2,6 +2,10 @@ import { type DataAppAnnotationDraft } from '../core/annotations.js';
|
|
|
2
2
|
import type { MessageTransport } from '../core/messages.js';
|
|
3
3
|
/** Deliver annotations to the authenticated host. Acknowledgement means retained, not sent to an agent. */
|
|
4
4
|
export declare function createAnnotationClient(transport: MessageTransport): {
|
|
5
|
+
/** Ask the host to submit its saved annotations using its existing send guards. */
|
|
6
|
+
requestSendAnnotations(options?: {
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}): Promise<null>;
|
|
5
9
|
/** Report the iframe editor's status so the host can guard batch submission. */
|
|
6
10
|
reportAnnotationEditorState(state: {
|
|
7
11
|
hasUnsavedChanges: boolean;
|
|
@@ -3,8 +3,7 @@ import type { DataTransport } from './transport.js';
|
|
|
3
3
|
import type { DataQueryBody, InputOf, OutputOf } from '../core/operation-types.js';
|
|
4
4
|
export type { InputOf, OutputOf } from '../core/operation-types.js';
|
|
5
5
|
/**
|
|
6
|
-
* Parsed operation data and query evidence.
|
|
7
|
-
* allowed.
|
|
6
|
+
* Parsed operation data and query evidence.
|
|
8
7
|
*/
|
|
9
8
|
export type DataResponse<Output, Input = unknown> = DataQueryBody<Output> & {
|
|
10
9
|
/** The exact browser input that produced this response. */
|
|
@@ -12,10 +12,11 @@ export declare function createIframeTransport({ parentOrigin, window: frame, tim
|
|
|
12
12
|
request: (message: RoutedMessage, signal?: AbortSignal) => Promise<unknown>;
|
|
13
13
|
transport: (operation: string, input: unknown, signal?: AbortSignal) => Promise<TransportResponse>;
|
|
14
14
|
lakehouse: {
|
|
15
|
-
queryAll(statement: string, { limit, signal }: {
|
|
15
|
+
queryAll(statement: string, { limit, signal, params }: {
|
|
16
16
|
limit: number;
|
|
17
17
|
signal: AbortSignal;
|
|
18
18
|
name?: string;
|
|
19
|
+
params?: import("../core/contract.js").QueryParameters;
|
|
19
20
|
}): Promise<import("../core/contract.js").QueryResult>;
|
|
20
21
|
};
|
|
21
22
|
dispose: () => void;
|
|
@@ -39,10 +40,11 @@ export declare function getDataAppTransport(frame?: Window): {
|
|
|
39
40
|
request: (message: RoutedMessage, signal?: AbortSignal) => Promise<unknown>;
|
|
40
41
|
transport: (operation: string, input: unknown, signal?: AbortSignal) => Promise<TransportResponse>;
|
|
41
42
|
lakehouse: {
|
|
42
|
-
queryAll(statement: string, { limit, signal }: {
|
|
43
|
+
queryAll(statement: string, { limit, signal, params }: {
|
|
43
44
|
limit: number;
|
|
44
45
|
signal: AbortSignal;
|
|
45
46
|
name?: string;
|
|
47
|
+
params?: import("../core/contract.js").QueryParameters;
|
|
46
48
|
}): Promise<import("../core/contract.js").QueryResult>;
|
|
47
49
|
};
|
|
48
50
|
dispose: () => void;
|
|
@@ -81,6 +81,8 @@ export declare const annotationDraftRoute: import("./messages.js").MessageRoute<
|
|
|
81
81
|
export declare const annotationEditorStateRoute: import("./messages.js").MessageRoute<{
|
|
82
82
|
hasUnsavedChanges: boolean;
|
|
83
83
|
}, null>;
|
|
84
|
+
/** Request host-owned batch submission without transferring ownership of drafts. */
|
|
85
|
+
export declare const annotationSendRoute: import("./messages.js").MessageRoute<null, null>;
|
|
84
86
|
export declare const annotationUpdateRoute: import("./messages.js").MessageRoute<{
|
|
85
87
|
id: string;
|
|
86
88
|
comment: string;
|
|
@@ -1,15 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* App
|
|
3
|
-
* @module @altertable/data-app
|
|
2
|
+
* App configuration shared by browser and server.
|
|
3
|
+
* @module @altertable/data-app
|
|
4
4
|
*/
|
|
5
|
+
import { defineOperation } from './contract.js';
|
|
5
6
|
import type { AppearanceOptions } from './appearance.js';
|
|
6
|
-
|
|
7
|
+
import { type QueryDefinitions } from './queries.js';
|
|
8
|
+
export type { QueryDefinitions, QueryParameterValue } from './queries.js';
|
|
9
|
+
export type DataAppConfig<Queries extends QueryDefinitions = QueryDefinitions> = {
|
|
7
10
|
title: string;
|
|
8
11
|
/** Display labels; scope does not grant data access. */
|
|
9
12
|
scope: {
|
|
10
13
|
organization: string;
|
|
11
14
|
environment: string;
|
|
12
15
|
};
|
|
13
|
-
|
|
16
|
+
/** Explain what the app helps readers explore. */
|
|
17
|
+
description: string;
|
|
18
|
+
/** Omit to use the standard appearance. */
|
|
19
|
+
appearance?: AppearanceOptions;
|
|
20
|
+
/** Named SQL statements and parameter defaults used by app operations. */
|
|
21
|
+
queries: Queries;
|
|
14
22
|
};
|
|
23
|
+
type ImmutableQueries<Queries extends QueryDefinitions> = {
|
|
24
|
+
readonly [Name in keyof Queries]: {
|
|
25
|
+
readonly statement: Queries[Name]['statement'];
|
|
26
|
+
readonly params: {
|
|
27
|
+
readonly [Param in keyof Queries[Name]['params']]: Readonly<Queries[Name]['params'][Param]>;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/** An app declaration with operations bound to its query registry. */
|
|
32
|
+
export type DataAppDefinition<Queries extends QueryDefinitions = QueryDefinitions> = Readonly<Omit<DataAppConfig<Queries>, 'queries'>> & {
|
|
33
|
+
readonly queries: ImmutableQueries<Queries>;
|
|
34
|
+
defineOperation<Input, Output>(operation: Omit<Parameters<typeof defineOperation<Input, Output, Queries>>[0], 'queries'>): ReturnType<typeof defineOperation<Input, Output, Queries>>;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Define the app's query source of truth while preserving exact query and parameter names.
|
|
38
|
+
* Use `app.defineOperation()` to bind operations to these queries. Pass the app once
|
|
39
|
+
* to `mountDataApp({ app, component })` or `<DataAppProvider app={app}>`.
|
|
40
|
+
* The app owns an immutable snapshot of its registry; later input mutations do not change it.
|
|
41
|
+
* This declaration does not execute queries or mount UI. Static apps use `queries: {}`.
|
|
42
|
+
*
|
|
43
|
+
* Declare exactly one module-level const initialized with `defineDataApp({ ... })` in the app's
|
|
44
|
+
* declaration source. For the future backend extractor, the argument is read before bundling
|
|
45
|
+
* by resolving the named `defineDataApp` import from `@altertable/data-app`, including aliases.
|
|
46
|
+
* Variable name is arbitrary;
|
|
47
|
+
* export only for imports by other modules. The argument must be a direct object literal
|
|
48
|
+
* containing schema-valid objects, arrays, strings (including templates without interpolation),
|
|
49
|
+
* finite signed numbers, booleans, and null. No spreads, computed keys, references, calls,
|
|
50
|
+
* or callbacks inside it. Extraction must validate data without evaluating app code.
|
|
51
|
+
* TypeScript checks the shape, not AST syntax. Keep executable composition outside the call.
|
|
52
|
+
*
|
|
53
|
+
* SQL passes unchanged to the backend, which interprets placeholders such as `$orgId`.
|
|
54
|
+
* Declare every parameter with `{ defaultValue: value }` or `{}` for a required value.
|
|
55
|
+
* Values are finite numbers, strings, booleans, or null, never SQL fragments or identifiers.
|
|
56
|
+
* Caller values override defaults; trusted server `queryParams` cannot be overridden.
|
|
57
|
+
*/
|
|
58
|
+
export declare function defineDataApp<const Queries extends QueryDefinitions>(config: DataAppConfig<Queries>): DataAppDefinition<Queries>;
|
|
15
59
|
export declare function dataAppTitle(config: Pick<DataAppConfig, 'title' | 'scope'>): string;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type QueryDefinitions, type QueryParameterValue } from './queries.js';
|
|
2
|
+
export type { QueryDefinitions, QueryParameters, QueryParameterValue, } from './queries.js';
|
|
1
3
|
import type { DateRange as DateRangeInput } from './date-range.js';
|
|
2
4
|
import type { QueryResult, DataOperation, OperationContext } from './operation-types.js';
|
|
3
5
|
export type { QueryResult, DisclosedQuery, Lakehouse, OperationContext, DataOperation, DataOperations, } from './operation-types.js';
|
|
@@ -69,26 +71,32 @@ export declare class DataSourceError extends Error {
|
|
|
69
71
|
static [Symbol.hasInstance](value: unknown): boolean;
|
|
70
72
|
constructor(reason: 'unauthorized' | 'forbidden' | 'rate_limited' | 'query_rejected' | 'unavailable', status?: number | undefined);
|
|
71
73
|
}
|
|
72
|
-
|
|
74
|
+
type RegisteredQueryParameters<Queries extends QueryDefinitions, Name extends keyof Queries> = keyof Queries[Name]['params'] extends never ? Record<string, never> : Partial<Record<keyof Queries[Name]['params'], QueryParameterValue>>;
|
|
75
|
+
export type RegisteredQuery<Queries extends QueryDefinitions> = <Name extends keyof Queries & string, const Params extends object = {}>(name: Name, params?: Params & NoInfer<RegisteredQueryParameters<Queries, Name>> & NoInfer<Record<Exclude<keyof Params, keyof Queries[Name]['params']>, never>> & NoInfer<Params extends (...args: never[]) => unknown ? never : object>, options?: {
|
|
73
76
|
limit?: number;
|
|
74
77
|
}) => Promise<QueryResult>;
|
|
75
|
-
export declare function defineOperation<Input, Output, const
|
|
76
|
-
|
|
77
|
-
run: (context: OperationContext & {
|
|
78
|
-
query:
|
|
78
|
+
export declare function defineOperation<Input, Output, const Queries extends QueryDefinitions>(operation: Omit<DataOperation<Input, Output>, 'run' | 'queryNames'> & {
|
|
79
|
+
queries: Queries;
|
|
80
|
+
run: (context: Pick<OperationContext, 'signal'> & {
|
|
81
|
+
query: RegisteredQuery<NoInfer<Queries>>;
|
|
79
82
|
}, input: Input) => Promise<Output>;
|
|
80
|
-
}): DataOperation<Input, Output> & {
|
|
81
|
-
queryNames
|
|
83
|
+
}): Omit<DataOperation<Input, Output>, 'queryNames'> & {
|
|
84
|
+
queryNames: {
|
|
85
|
+
readonly [Name in keyof Queries & string]: Name;
|
|
86
|
+
};
|
|
82
87
|
};
|
|
83
|
-
|
|
84
|
-
|
|
88
|
+
/** A connectivity probe uses the app's registered connection query. */
|
|
89
|
+
export declare function connectionCheck<const Queries extends QueryDefinitions & {
|
|
90
|
+
connection: QueryDefinitions[string];
|
|
91
|
+
}>(queries: Queries): Omit<DataOperation<Record<string, never>, true>, "queryNames"> & {
|
|
92
|
+
queryNames: { readonly [Name in keyof Queries & string]: Name; };
|
|
85
93
|
};
|
|
86
|
-
/** Success requires a bounded SQL query; it does not establish access to a particular dataset. */
|
|
87
|
-
export declare function connectionCheck(): DataOperation<Record<string, never>, true>;
|
|
88
94
|
export { dimensionFilter, parseDimensionSelection, parseFacetOptions, dimensionPredicate, } from './dimension.js';
|
|
89
95
|
export type { DimensionSelection, DimensionMember, DimensionOption, DimensionVariable, DimensionFilterOptions, } from './dimension.js';
|
|
90
96
|
export { createMessageRouter, defineMessageRoute, defineDataQueryRoute, MessageRoutingError, dataAppRoutes, navigationUpdateRoute, sqlQueryRoute, } from './messages.js';
|
|
91
97
|
export type { MessageContext, RoutedMessage, MessageDispatcher, MessageTransport, MessageRoute, MessageRoutes, MessageInput, MessageHandlers, MessageOutput, OperationContracts, DataQueryInput, DataQueryBody, DataQueryRoute, NavigationUpdate, SqlQueryInput, } from './messages.js';
|
|
92
98
|
export type { TransportResponse } from './bridge.js';
|
|
93
|
-
export { annotationDraftRoute, annotationModeRoute, annotationEditorStateRoute, annotationUpdateRoute, parseDataAppAnnotationDraft, } from './annotations.js';
|
|
99
|
+
export { annotationDraftRoute, annotationModeRoute, annotationEditorStateRoute, annotationSendRoute, annotationUpdateRoute, parseDataAppAnnotationDraft, } from './annotations.js';
|
|
94
100
|
export type { DataAppAnnotationDraft, DataAppAnnotationPresentation, } from './annotations.js';
|
|
101
|
+
export { numberFilter, booleanFilter, parseNumberSelection, parseBooleanSelection, } from './filters.js';
|
|
102
|
+
export type { NumberSelection, NumberOperator, NumberFilter, BooleanSelection, BooleanFilter, } from './filters.js';
|
|
@@ -9,7 +9,7 @@ export type DimensionMember<T extends DimensionValue = DimensionValue> = {
|
|
|
9
9
|
export type DimensionSelection<T extends DimensionValue = DimensionValue> = {
|
|
10
10
|
kind: 'all';
|
|
11
11
|
} | {
|
|
12
|
-
kind: 'include';
|
|
12
|
+
kind: 'include' | 'exclude';
|
|
13
13
|
members: readonly DimensionMember<T>[];
|
|
14
14
|
};
|
|
15
15
|
export type DimensionOption<T extends DimensionValue = DimensionValue> = {
|
|
@@ -17,11 +17,12 @@ export type DimensionOption<T extends DimensionValue = DimensionValue> = {
|
|
|
17
17
|
label: string;
|
|
18
18
|
count?: number;
|
|
19
19
|
};
|
|
20
|
-
export type DimensionVariable<T extends DimensionValue = DimensionValue> =
|
|
20
|
+
export type DimensionVariable<T extends DimensionValue = DimensionValue> = AppVariable<DimensionSelection<T>, 'dimension'> & {
|
|
21
21
|
label: string;
|
|
22
|
-
|
|
22
|
+
selectionMode: 'single' | 'multiple';
|
|
23
23
|
valueType: 'string' | 'number';
|
|
24
24
|
allowMissing: boolean;
|
|
25
|
+
allowExclusion: boolean;
|
|
25
26
|
maxSelected: number;
|
|
26
27
|
options: readonly DimensionOption<T>[];
|
|
27
28
|
facet?: {
|
|
@@ -35,8 +36,9 @@ export type DimensionFilterOptions<T extends DimensionValue> = {
|
|
|
35
36
|
key: string;
|
|
36
37
|
label: string;
|
|
37
38
|
valueType: T extends number ? 'number' : 'string';
|
|
38
|
-
|
|
39
|
+
selectionMode: 'single' | 'multiple';
|
|
39
40
|
allowMissing?: boolean;
|
|
41
|
+
allowExclusion?: boolean;
|
|
40
42
|
maxSelected?: number;
|
|
41
43
|
history?: HistoryMode;
|
|
42
44
|
} & ({
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AppVariable, HistoryMode } from './variables.js';
|
|
2
|
+
export type NumberOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte';
|
|
3
|
+
export type NumberSelection = {
|
|
4
|
+
kind: 'all';
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'comparison';
|
|
7
|
+
operator: NumberOperator;
|
|
8
|
+
value: number;
|
|
9
|
+
} | {
|
|
10
|
+
kind: 'range';
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
};
|
|
14
|
+
export type BooleanSelection = {
|
|
15
|
+
kind: 'all';
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'is';
|
|
18
|
+
value: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type NumberFilter = AppVariable<NumberSelection, 'numberFilter'> & {
|
|
21
|
+
label: string;
|
|
22
|
+
min?: number;
|
|
23
|
+
max?: number;
|
|
24
|
+
};
|
|
25
|
+
export type BooleanFilter = AppVariable<BooleanSelection, 'booleanFilter'> & {
|
|
26
|
+
label: string;
|
|
27
|
+
};
|
|
28
|
+
type FilterOptions<Value> = {
|
|
29
|
+
key: string;
|
|
30
|
+
label: string;
|
|
31
|
+
defaultValue?: Value;
|
|
32
|
+
history?: HistoryMode;
|
|
33
|
+
};
|
|
34
|
+
export declare const numberOperatorLabels: Record<NumberOperator, string>;
|
|
35
|
+
/** Exact numeric predicates; range bounds are inclusive and may be open-ended. */
|
|
36
|
+
export declare function numberFilter({ key, label, min, max, defaultValue, history, }: FilterOptions<NumberSelection> & {
|
|
37
|
+
min?: number;
|
|
38
|
+
max?: number;
|
|
39
|
+
}): NumberFilter;
|
|
40
|
+
/** Boolean predicate with a distinct unrestricted state. */
|
|
41
|
+
export declare function booleanFilter({ key, label, defaultValue, history, }: FilterOptions<BooleanSelection>): BooleanFilter;
|
|
42
|
+
export declare function parseNumberSelection(value: unknown, filter: NumberFilter): NumberSelection;
|
|
43
|
+
export declare function parseBooleanSelection(value: unknown, filter: BooleanFilter): BooleanSelection;
|
|
44
|
+
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type QueryParameters } from './queries.js';
|
|
1
2
|
import type { TransportResponse, QueryResult, OperationContracts, DataQueryInput, DataQueryBody, OutputOf } from './operation-types.js';
|
|
2
3
|
export type { OperationContracts, DataQueryInput, DataQueryBody, } from './operation-types.js';
|
|
3
4
|
import { type NavigationUpdate } from './navigation.js';
|
|
@@ -49,6 +50,7 @@ export declare const navigationUpdateRoute: MessageRoute<NavigationUpdate, null>
|
|
|
49
50
|
export type SqlQueryInput = {
|
|
50
51
|
statement: string;
|
|
51
52
|
limit: number;
|
|
53
|
+
params?: QueryParameters;
|
|
52
54
|
};
|
|
53
55
|
/** SQL delivery for browser-owned operations. Hosts must enforce backend access and resource limits. */
|
|
54
56
|
export declare const sqlQueryRoute: MessageRoute<SqlQueryInput, QueryResult>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** Shared operation, query, and response contracts; independent of delivery adapters. */
|
|
2
|
+
import type { QueryParameters } from './queries.js';
|
|
2
3
|
export type OperationContracts = Record<string, {
|
|
3
4
|
input: (value: unknown) => unknown;
|
|
4
5
|
output: (value: unknown) => unknown;
|
|
@@ -17,10 +18,11 @@ export type QueryResult = {
|
|
|
17
18
|
rows: unknown[][];
|
|
18
19
|
queryId?: string;
|
|
19
20
|
};
|
|
20
|
-
/** One
|
|
21
|
+
/** One registered statement an operation ran, with its parameter values. */
|
|
21
22
|
export type DisclosedQuery = {
|
|
22
23
|
name: string;
|
|
23
24
|
statement: string;
|
|
25
|
+
params?: QueryParameters;
|
|
24
26
|
queryId?: string;
|
|
25
27
|
};
|
|
26
28
|
/** Query interface supplied by a server adapter or an authorized iframe bridge. */
|
|
@@ -29,11 +31,13 @@ export type Lakehouse = {
|
|
|
29
31
|
limit: number;
|
|
30
32
|
signal: AbortSignal;
|
|
31
33
|
name?: string;
|
|
34
|
+
params?: QueryParameters;
|
|
32
35
|
}): Promise<QueryResult>;
|
|
33
36
|
};
|
|
34
37
|
export type OperationContext = {
|
|
35
38
|
lakehouse: Lakehouse;
|
|
36
39
|
signal: AbortSignal;
|
|
40
|
+
queryParams?: QueryParameters;
|
|
37
41
|
};
|
|
38
42
|
/**
|
|
39
43
|
* Parsers run in the operation executor's runtime: server for HTTP apps, browser for bundle apps.
|
|
@@ -49,7 +53,6 @@ export type DataOperation<Input, Output> = {
|
|
|
49
53
|
maxQueryRows: number;
|
|
50
54
|
maxDurationMs: number;
|
|
51
55
|
maxResponseBytes?: number;
|
|
52
|
-
exposeSql?: boolean;
|
|
53
56
|
};
|
|
54
57
|
};
|
|
55
58
|
/** Heterogeneous registry: inputs vary per operation, so checks retain unknown values. */
|