@altertable/data-app 0.61.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +5 -3
- package/CONTRIBUTING.md +7 -4
- package/README.md +14 -14
- package/dist/chunks/{contract-ehk50k7e.js → contract-3bnrf5pf.js} +25 -3
- package/dist/chunks/contract-3bnrf5pf.js.map +10 -0
- package/dist/chunks/{contract-zr4s2g7m.js → contract-4vrw9zk9.js} +41 -11
- package/dist/chunks/contract-4vrw9zk9.js.map +12 -0
- package/dist/chunks/{contract-14vxdcrs.js → contract-farfe948.js} +17 -21
- package/dist/chunks/contract-farfe948.js.map +10 -0
- package/dist/chunks/contract-nt819swq.js.map +1 -1
- package/dist/chunks/{contract-7gpsee0v.js → contract-rrm7s5zp.js} +48 -19
- package/dist/chunks/contract-rrm7s5zp.js.map +12 -0
- package/dist/chunks/{contract-d9skd1n7.js → contract-xjv197ck.js} +161 -14
- package/dist/chunks/contract-xjv197ck.js.map +13 -0
- package/dist/client/index.js +6 -4
- package/dist/client/index.js.map +1 -1
- package/dist/core/appearance.js +1 -1
- package/dist/core/contract.js +5 -3
- package/dist/core/contract.js.map +1 -1
- package/dist/embed/index.js +42 -8
- package/dist/embed/index.js.map +5 -4
- package/dist/local.js +191 -79
- package/dist/local.js.map +8 -7
- package/dist/react/embed/index.js +84 -99
- package/dist/react/embed/index.js.map +4 -5
- package/dist/react/index.css +156 -6
- package/dist/react/index.js +313 -152
- package/dist/react/index.js.map +17 -13
- package/dist/server.js +172 -76
- package/dist/server.js.map +7 -6
- package/dist/types/client/iframe.d.ts +14 -0
- package/dist/types/client/index.d.ts +19 -13
- package/dist/types/core/appearance.d.ts +7 -6
- package/dist/types/core/contract.d.ts +3 -3
- package/dist/types/core/messages.d.ts +7 -1
- package/dist/types/core/operation.d.ts +27 -0
- package/dist/types/core/presentation.d.ts +7 -0
- package/dist/types/embed/bridge.d.ts +12 -0
- package/dist/types/embed/host.d.ts +10 -3
- package/dist/types/embed/index.d.ts +7 -4
- package/dist/types/embed/{shell.d.ts → source.d.ts} +5 -4
- package/dist/types/embed/sql.d.ts +4 -0
- package/dist/types/react/embed/bridge.d.ts +19 -10
- package/dist/types/react/embed/index.d.ts +0 -2
- package/dist/types/react/ui/DataAppSkeleton.d.ts +8 -0
- package/dist/types/react/ui/SearchInput.d.ts +1 -1
- package/dist/types/react/ui/index.d.ts +2 -0
- package/dist/types/react/ui/useAppAppearance.d.ts +3 -0
- package/dist/types/react/ui/useDataAppPresentation.d.ts +3 -0
- package/dist/types/react/view-controls.d.ts +1 -1
- package/dist/{bootstrap.js → worker.js} +144 -12
- package/docs/app-authoring.md +24 -4
- package/docs/appearance.md +21 -8
- package/docs/client.md +39 -2
- package/docs/contract.md +2 -2
- package/docs/embed.md +105 -18
- package/docs/react-embed.md +84 -24
- package/docs/react.md +51 -0
- package/docs/server-bun.md +5 -0
- package/docs/worker.md +57 -0
- package/package.json +3 -3
- package/dist/chunks/contract-14vxdcrs.js.map +0 -10
- package/dist/chunks/contract-7gpsee0v.js.map +0 -11
- package/dist/chunks/contract-d9skd1n7.js.map +0 -12
- package/dist/chunks/contract-ehk50k7e.js.map +0 -10
- package/dist/chunks/contract-zr4s2g7m.js.map +0 -12
- package/dist/types/embed/standalone.d.ts +0 -1
- package/dist/types/react/embed/shell.d.ts +0 -10
- package/docs/bootstrap.md +0 -76
package/dist/embed/index.js.map
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/embed/bootstrap.ts", "../src/embed/navigation.ts"],
|
|
3
|
+
"sources": ["../src/embed/bootstrap.ts", "../src/embed/navigation.ts", "../src/embed/sql.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
5
|
"import {\n createIframeTransport,\n installDataAppTransport,\n} from '@/src/client/iframe';\n\n/** Run from a trusted bootstrap page in an allow-scripts-only iframe. CSP can prohibit network access. */\nexport function startDataAppBootstrap({\n parentOrigin,\n window: frame = window,\n}: {\n parentOrigin: string;\n window?: Window;\n}) {\n let loaded: string | undefined;\n let failed = false;\n const bridge = createIframeTransport({\n parentOrigin,\n window: frame,\n mode: 'bundle',\n loadScript(javascript) {\n if (loaded !== undefined) {\n if (javascript === loaded) {\n if (failed) bridge.fail();\n else bridge.ready();\n } else bridge.fail();\n\n return;\n }\n loaded = javascript;\n try {\n const script = frame.document.createElement('script');\n script.textContent = javascript;\n frame.document.body.append(script);\n if (!failed) bridge.ready();\n } catch {\n reportError();\n }\n },\n });\n\n function reportError() {\n failed = true;\n bridge.fail();\n }\n\n frame.addEventListener('error', reportError);\n frame.addEventListener('unhandledrejection', reportError);\n const uninstall = installDataAppTransport(bridge, frame);\n\n return () => {\n frame.removeEventListener('error', reportError);\n frame.removeEventListener('unhandledrejection', reportError);\n uninstall();\n };\n}\n",
|
|
6
|
-
"import { PARENT_PARAM } from '@/src/core/bridge';\nimport type { NavigationUpdate } from '@/src/core/messages';\n\n/** Override this handler for framework routers. Reserved host parameters survive app navigation. */\nexport function createNavigationHandler({\n window: host = window,\n reservedSearchParams = [],\n}: {\n window?: Window;\n reservedSearchParams?: readonly string[];\n} = {}) {\n function navigate(input: NavigationUpdate): null {\n const previous = new URL(host.location.href);\n const url = new URL(previous);\n url.search = input.search;\n url.searchParams.delete(PARENT_PARAM);\n for (const name of reservedSearchParams) {\n url.searchParams.delete(name);\n for (const value of previous.searchParams.getAll(name))\n url.searchParams.append(name, value);\n }\n url.hash = input.hash;\n if (url.href !== host.location.href)\n host.history[input.mode === 'push' ? 'pushState' : 'replaceState'](\n null,\n '',\n url\n );\n if (input.title !== undefined) host.document.title = input.title;\n\n return null;\n }\n\n return navigate;\n}\n"
|
|
6
|
+
"import { PARENT_PARAM } from '@/src/core/bridge';\nimport type { NavigationUpdate } from '@/src/core/messages';\n\n/** Override this handler for framework routers. Reserved host parameters survive app navigation. */\nexport function createNavigationHandler({\n window: host = window,\n reservedSearchParams = [],\n}: {\n window?: Window;\n reservedSearchParams?: readonly string[];\n} = {}) {\n function navigate(input: NavigationUpdate): null {\n const previous = new URL(host.location.href);\n const url = new URL(previous);\n url.search = input.search;\n url.searchParams.delete(PARENT_PARAM);\n for (const name of reservedSearchParams) {\n url.searchParams.delete(name);\n for (const value of previous.searchParams.getAll(name))\n url.searchParams.append(name, value);\n }\n url.hash = input.hash;\n if (url.href !== host.location.href)\n host.history[input.mode === 'push' ? 'pushState' : 'replaceState'](\n null,\n '',\n url\n );\n if (input.title !== undefined) host.document.title = input.title;\n\n return null;\n }\n\n return navigate;\n}\n",
|
|
7
|
+
"import type { Lakehouse } from '@/src/core/contract';\nimport { toDataOperationFailure } from '@/src/core/operation';\nimport {\n MessageRoutingError,\n type MessageContext,\n type SqlQueryInput,\n} from '@/src/core/messages';\n\n/** Authorize every SQL request and supply a backend that independently enforces access and query limits. */\nexport function createSqlQueryHandler(\n authorize: (\n query: SqlQueryInput,\n context: MessageContext\n ) => Promise<Lakehouse>\n) {\n async function handleSqlQuery(input: SqlQueryInput, context: MessageContext) {\n const requestId = crypto.randomUUID();\n context.signal.throwIfAborted();\n let lakehouse: Lakehouse;\n try {\n lakehouse = await authorize(input, context);\n } catch {\n context.signal.throwIfAborted();\n throw new MessageRoutingError(\n 'forbidden',\n 'You cannot run this query.',\n requestId\n );\n }\n context.signal.throwIfAborted();\n try {\n return await lakehouse.queryAll(input.statement, {\n limit: input.limit,\n signal: context.signal,\n });\n } catch (error) {\n context.signal.throwIfAborted();\n if (error instanceof MessageRoutingError) throw error;\n const failure = toDataOperationFailure(error);\n throw new MessageRoutingError(failure.code, failure.message, requestId);\n }\n }\n\n return handleSqlQuery;\n}\n"
|
|
7
8
|
],
|
|
8
|
-
"mappings": "
|
|
9
|
-
"debugId": "
|
|
9
|
+
"mappings": "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",
|
|
10
|
+
"debugId": "10E8E3B80241E0BC64756E2164756E21",
|
|
10
11
|
"names": []
|
|
11
12
|
}
|
package/dist/local.js
CHANGED
|
@@ -12,6 +12,31 @@ function invariant(condition, message) {
|
|
|
12
12
|
}
|
|
13
13
|
// src/core/messages.ts
|
|
14
14
|
var errorBrand = Symbol.for("@altertable/data-app/MessageRoutingError");
|
|
15
|
+
function defineMessageRoute(route) {
|
|
16
|
+
return route;
|
|
17
|
+
}
|
|
18
|
+
var sqlQueryRoute = defineMessageRoute({
|
|
19
|
+
input(value) {
|
|
20
|
+
if (!value || typeof value !== "object")
|
|
21
|
+
throw new Error("Invalid SQL query.");
|
|
22
|
+
const query = value;
|
|
23
|
+
if (typeof query.statement !== "string" || !query.statement.trim() || typeof query.limit !== "number" || !Number.isSafeInteger(query.limit) || query.limit < 1)
|
|
24
|
+
throw new Error("Invalid SQL query.");
|
|
25
|
+
return { statement: query.statement, limit: query.limit };
|
|
26
|
+
},
|
|
27
|
+
output(value, input) {
|
|
28
|
+
if (!value || typeof value !== "object")
|
|
29
|
+
throw new Error("Invalid query result.");
|
|
30
|
+
const result = value;
|
|
31
|
+
if (!Array.isArray(result.columns) || !result.columns.every((column) => column && typeof column.name === "string" && (column.type === undefined || typeof column.type === "string")) || !Array.isArray(result.rows) || result.rows.length > input.limit || !result.rows.every((row) => Array.isArray(row) && row.length === result.columns.length) || result.queryId !== undefined && typeof result.queryId !== "string")
|
|
32
|
+
throw new Error("Invalid query result.");
|
|
33
|
+
return {
|
|
34
|
+
columns: result.columns,
|
|
35
|
+
rows: result.rows,
|
|
36
|
+
...result.queryId === undefined ? {} : { queryId: result.queryId }
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
15
40
|
// src/core/contract.ts
|
|
16
41
|
function defineQueryNames(names) {
|
|
17
42
|
const values = Object.values(names);
|
|
@@ -34,14 +59,146 @@ var connectionQueryNames = defineQueryNames({
|
|
|
34
59
|
connection: "connection-check"
|
|
35
60
|
});
|
|
36
61
|
|
|
37
|
-
// src/
|
|
62
|
+
// src/core/operation.ts
|
|
63
|
+
async function executeDataOperation(operation, value, {
|
|
64
|
+
operationName,
|
|
65
|
+
lakehouse,
|
|
66
|
+
signal: requestSignal,
|
|
67
|
+
includeSql,
|
|
68
|
+
requestId
|
|
69
|
+
}) {
|
|
70
|
+
requestSignal.throwIfAborted();
|
|
71
|
+
let input;
|
|
72
|
+
try {
|
|
73
|
+
input = operation.input(value);
|
|
74
|
+
} catch {
|
|
75
|
+
throw new OperationInputError;
|
|
76
|
+
}
|
|
77
|
+
const signal = AbortSignal.any([
|
|
78
|
+
requestSignal,
|
|
79
|
+
AbortSignal.timeout(operation.policy.maxDurationMs)
|
|
80
|
+
]);
|
|
81
|
+
const execution = createOperationLakehouse(operation, lakehouse, signal, operationName);
|
|
82
|
+
try {
|
|
83
|
+
const output = await runOperation(operation, input, {
|
|
84
|
+
lakehouse: execution.lakehouse,
|
|
85
|
+
signal
|
|
86
|
+
});
|
|
87
|
+
const body = {
|
|
88
|
+
data: operation.output(output),
|
|
89
|
+
requestId,
|
|
90
|
+
queriedAt: new Date().toISOString(),
|
|
91
|
+
queryIds: execution.queryIds,
|
|
92
|
+
...operation.policy.exposeSql && includeSql ? { queries: execution.queries } : {}
|
|
93
|
+
};
|
|
94
|
+
const serializedBody = JSON.stringify(body);
|
|
95
|
+
const responseBytes = new TextEncoder().encode(serializedBody).byteLength;
|
|
96
|
+
if (responseBytes > (operation.policy.maxResponseBytes ?? 1e6))
|
|
97
|
+
throw new Error("Response exceeds byte limit.");
|
|
98
|
+
return { body, serializedBody };
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (signal.aborted && !(error instanceof DataSourceError))
|
|
101
|
+
throw new OperationTimeoutError;
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function createOperationLakehouse(operation, source, signal, operationName) {
|
|
106
|
+
const queryIds = [];
|
|
107
|
+
const queries = [];
|
|
108
|
+
const lakehouse = {
|
|
109
|
+
async queryAll(statement, options) {
|
|
110
|
+
if (!Number.isInteger(options.limit) || options.limit < 1)
|
|
111
|
+
throw new Error("Query needs a positive row limit.");
|
|
112
|
+
if (operation.queryNames && !Object.values(operation.queryNames).includes(options.name ?? ""))
|
|
113
|
+
throw new Error(`Query name is not registered for operation ${operationName}.`);
|
|
114
|
+
const query = {
|
|
115
|
+
name: options.name ?? `Query ${queries.length + 1}`,
|
|
116
|
+
statement
|
|
117
|
+
};
|
|
118
|
+
queries.push(query);
|
|
119
|
+
const limit = Math.min(options.limit, operation.policy.maxQueryRows);
|
|
120
|
+
let result;
|
|
121
|
+
try {
|
|
122
|
+
result = await source.queryAll(statement, {
|
|
123
|
+
limit,
|
|
124
|
+
signal: AbortSignal.any([signal, options.signal])
|
|
125
|
+
});
|
|
126
|
+
} catch (error) {
|
|
127
|
+
if (error instanceof DataSourceError)
|
|
128
|
+
error.queryName = query.name;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
if (result.rows.length > limit)
|
|
132
|
+
throw new Error("Result exceeds row limit.");
|
|
133
|
+
if (result.queryId) {
|
|
134
|
+
query.queryId = result.queryId;
|
|
135
|
+
queryIds.push(result.queryId);
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
return { lakehouse, queries, queryIds };
|
|
141
|
+
}
|
|
142
|
+
async function runOperation(operation, input, { lakehouse, signal }) {
|
|
143
|
+
let abort;
|
|
144
|
+
try {
|
|
145
|
+
signal.throwIfAborted();
|
|
146
|
+
const cancelled = new Promise((_, reject) => {
|
|
147
|
+
function rejectAborted() {
|
|
148
|
+
reject(new Error("Request aborted."));
|
|
149
|
+
}
|
|
150
|
+
abort = rejectAborted;
|
|
151
|
+
signal.addEventListener("abort", rejectAborted, { once: true });
|
|
152
|
+
if (signal.aborted)
|
|
153
|
+
rejectAborted();
|
|
154
|
+
});
|
|
155
|
+
return await Promise.race([
|
|
156
|
+
cancelled,
|
|
157
|
+
Promise.resolve().then(() => {
|
|
158
|
+
signal.throwIfAborted();
|
|
159
|
+
return operation.run({ lakehouse, signal }, input);
|
|
160
|
+
})
|
|
161
|
+
]);
|
|
162
|
+
} finally {
|
|
163
|
+
if (abort)
|
|
164
|
+
signal.removeEventListener("abort", abort);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
class OperationTimeoutError extends Error {
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
class OperationInputError extends Error {
|
|
172
|
+
}
|
|
38
173
|
var sourceErrorMessages = {
|
|
39
|
-
unauthorized: "
|
|
40
|
-
forbidden: "This app cannot access
|
|
174
|
+
unauthorized: "This app could not authenticate its data connection. Contact the app owner.",
|
|
175
|
+
forbidden: "This app cannot access the requested data. Contact the app owner.",
|
|
41
176
|
rate_limited: "The lakehouse is busy. Wait a moment and retry.",
|
|
42
177
|
query_rejected: "A lakehouse query was rejected. Check the app's data operation.",
|
|
43
178
|
unavailable: "The lakehouse is unavailable. Check the connection and retry."
|
|
44
179
|
};
|
|
180
|
+
function toDataOperationFailure(error, aborted = false) {
|
|
181
|
+
if (error instanceof OperationInputError)
|
|
182
|
+
return {
|
|
183
|
+
status: 400,
|
|
184
|
+
code: "invalid_input",
|
|
185
|
+
message: "Check this operation's inputs."
|
|
186
|
+
};
|
|
187
|
+
if (error instanceof DataSourceError)
|
|
188
|
+
return {
|
|
189
|
+
status: error.reason === "rate_limited" ? 429 : 502,
|
|
190
|
+
code: `source_${error.reason}`,
|
|
191
|
+
message: sourceErrorMessages[error.reason]
|
|
192
|
+
};
|
|
193
|
+
const timedOut = aborted || error instanceof OperationTimeoutError;
|
|
194
|
+
return {
|
|
195
|
+
status: timedOut ? 504 : 502,
|
|
196
|
+
code: timedOut ? "timeout" : "query_failed",
|
|
197
|
+
message: timedOut ? "The data request timed out." : "The data request failed."
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/server/handler.ts
|
|
45
202
|
function createDataHandler(operations, authorize) {
|
|
46
203
|
async function handleDataRequest(request) {
|
|
47
204
|
const requestId = crypto.randomUUID();
|
|
@@ -80,78 +237,22 @@ function createDataHandler(operations, authorize) {
|
|
|
80
237
|
return problem(504, "timeout", "The data request timed out.", requestId);
|
|
81
238
|
try {
|
|
82
239
|
const body = await readInput(request);
|
|
83
|
-
input =
|
|
240
|
+
input = JSON.parse(body);
|
|
84
241
|
} catch (error) {
|
|
85
242
|
if (error instanceof InputTooLargeError)
|
|
86
243
|
return problem(413, "input_too_large", "Input is too large.", requestId);
|
|
87
244
|
return problem(400, "invalid_input", "Check this operation's inputs.", requestId);
|
|
88
245
|
}
|
|
89
|
-
const
|
|
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;
|
|
246
|
+
const signal = request.signal;
|
|
125
247
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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,
|
|
248
|
+
const { serializedBody } = await executeDataOperation(operation, input, {
|
|
249
|
+
lakehouse: access.lakehouse,
|
|
250
|
+
includeSql: access.canDiscloseSql,
|
|
251
|
+
signal,
|
|
145
252
|
requestId,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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, {
|
|
253
|
+
operationName: name
|
|
254
|
+
});
|
|
255
|
+
return new Response(serializedBody, {
|
|
155
256
|
headers: {
|
|
156
257
|
"cache-control": "no-store",
|
|
157
258
|
"content-type": "application/json"
|
|
@@ -160,13 +261,8 @@ function createDataHandler(operations, authorize) {
|
|
|
160
261
|
} catch (error) {
|
|
161
262
|
const source = error instanceof DataSourceError ? error : null;
|
|
162
263
|
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
|
-
|
|
164
|
-
|
|
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);
|
|
264
|
+
const failure = toDataOperationFailure(error, signal.aborted);
|
|
265
|
+
return problem(failure.status, failure.code, failure.message, requestId);
|
|
170
266
|
}
|
|
171
267
|
}
|
|
172
268
|
return handleDataRequest;
|
|
@@ -310,23 +406,39 @@ function parseQueryResult(body, limit) {
|
|
|
310
406
|
if (!lines.length)
|
|
311
407
|
throw new Error("Query response is empty.");
|
|
312
408
|
const metadata = JSON.parse(lines[0]);
|
|
409
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata) || "query_id" in metadata && metadata.query_id !== undefined && typeof metadata.query_id !== "string")
|
|
410
|
+
throw new Error("Invalid query metadata.");
|
|
411
|
+
const queryId = "query_id" in metadata ? metadata.query_id : undefined;
|
|
313
412
|
let firstRowIndex = 1;
|
|
314
413
|
let columns = [];
|
|
315
414
|
if (lines[firstRowIndex]) {
|
|
316
415
|
const columnDefinitions = JSON.parse(lines[firstRowIndex]);
|
|
317
416
|
if (Array.isArray(columnDefinitions) && columnDefinitions.every((value) => typeof value === "string" || typeof value === "object" && value !== null && ("name" in value))) {
|
|
318
|
-
columns = columnDefinitions.map((value) => typeof value === "string" ? { name: value } : value);
|
|
417
|
+
columns = columnDefinitions.map((value) => typeof value === "string" ? { name: value } : parseColumn(value));
|
|
319
418
|
firstRowIndex++;
|
|
320
419
|
}
|
|
321
420
|
}
|
|
322
|
-
const rows = lines.slice(firstRowIndex).map((line) =>
|
|
421
|
+
const rows = lines.slice(firstRowIndex).map((line) => {
|
|
422
|
+
const row = JSON.parse(line);
|
|
423
|
+
if (!Array.isArray(row) || firstRowIndex === 2 && row.length !== columns.length)
|
|
424
|
+
throw new Error("Invalid query row.");
|
|
425
|
+
return row;
|
|
426
|
+
});
|
|
323
427
|
if (rows.length > limit)
|
|
324
428
|
throw new Error("Lakehouse returned more rows than requested.");
|
|
325
|
-
return { columns, rows, queryId
|
|
429
|
+
return { columns, rows, queryId };
|
|
430
|
+
}
|
|
431
|
+
function parseColumn(value) {
|
|
432
|
+
if (!value || typeof value !== "object" || !("name" in value) || typeof value.name !== "string" || "type" in value && value.type !== undefined && typeof value.type !== "string")
|
|
433
|
+
throw new Error("Invalid query column.");
|
|
434
|
+
return {
|
|
435
|
+
name: value.name,
|
|
436
|
+
..."type" in value && value.type !== undefined ? { type: value.type } : {}
|
|
437
|
+
};
|
|
326
438
|
}
|
|
327
439
|
export {
|
|
328
440
|
localLakehouse,
|
|
329
441
|
serveLocalApp
|
|
330
442
|
};
|
|
331
443
|
|
|
332
|
-
//# debugId=
|
|
444
|
+
//# debugId=34D393952628512F64756E2164756E21
|