@altertable/data-app 0.59.1 → 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.
Files changed (71) hide show
  1. package/AGENTS.md +5 -3
  2. package/CONTRIBUTING.md +16 -4
  3. package/README.md +20 -14
  4. package/dist/chunks/{contract-jksbmt5q.js → contract-3bnrf5pf.js} +27 -5
  5. package/dist/chunks/contract-3bnrf5pf.js.map +10 -0
  6. package/dist/chunks/{contract-tkkc28tg.js → contract-4vrw9zk9.js} +43 -13
  7. package/dist/chunks/contract-4vrw9zk9.js.map +12 -0
  8. package/dist/chunks/{contract-14vxdcrs.js → contract-farfe948.js} +17 -21
  9. package/dist/chunks/contract-farfe948.js.map +10 -0
  10. package/dist/chunks/contract-nt819swq.js.map +1 -1
  11. package/dist/chunks/{contract-tqrf3ykr.js → contract-rrm7s5zp.js} +61 -32
  12. package/dist/chunks/contract-rrm7s5zp.js.map +12 -0
  13. package/dist/chunks/{contract-mb5nfzwg.js → contract-xjv197ck.js} +178 -31
  14. package/dist/chunks/contract-xjv197ck.js.map +13 -0
  15. package/dist/client/index.js +6 -4
  16. package/dist/client/index.js.map +1 -1
  17. package/dist/core/appearance.js +1 -1
  18. package/dist/core/contract.js +5 -3
  19. package/dist/core/contract.js.map +1 -1
  20. package/dist/embed/index.js +42 -8
  21. package/dist/embed/index.js.map +5 -4
  22. package/dist/local.js +191 -79
  23. package/dist/local.js.map +8 -7
  24. package/dist/react/embed/index.js +84 -99
  25. package/dist/react/embed/index.js.map +4 -5
  26. package/dist/react/index.css +156 -6
  27. package/dist/react/index.js +313 -152
  28. package/dist/react/index.js.map +17 -13
  29. package/dist/server.js +172 -76
  30. package/dist/server.js.map +7 -6
  31. package/dist/types/client/iframe.d.ts +14 -0
  32. package/dist/types/client/index.d.ts +19 -13
  33. package/dist/types/core/appearance.d.ts +7 -6
  34. package/dist/types/core/contract.d.ts +3 -3
  35. package/dist/types/core/messages.d.ts +9 -3
  36. package/dist/types/core/operation.d.ts +27 -0
  37. package/dist/types/core/presentation.d.ts +7 -0
  38. package/dist/types/embed/bridge.d.ts +12 -0
  39. package/dist/types/embed/host.d.ts +10 -3
  40. package/dist/types/embed/index.d.ts +7 -4
  41. package/dist/types/embed/{shell.d.ts → source.d.ts} +5 -4
  42. package/dist/types/embed/sql.d.ts +4 -0
  43. package/dist/types/react/embed/bridge.d.ts +19 -10
  44. package/dist/types/react/embed/index.d.ts +0 -2
  45. package/dist/types/react/ui/DataAppSkeleton.d.ts +8 -0
  46. package/dist/types/react/ui/SearchInput.d.ts +1 -1
  47. package/dist/types/react/ui/index.d.ts +2 -0
  48. package/dist/types/react/ui/useAppAppearance.d.ts +3 -0
  49. package/dist/types/react/ui/useDataAppPresentation.d.ts +3 -0
  50. package/dist/types/react/view-controls.d.ts +1 -1
  51. package/dist/{bootstrap.js → worker.js} +161 -29
  52. package/docs/app-authoring.md +24 -4
  53. package/docs/appearance.md +21 -8
  54. package/docs/client.md +40 -3
  55. package/docs/contract.md +9 -7
  56. package/docs/embed.md +114 -19
  57. package/docs/react-embed.md +84 -24
  58. package/docs/react.md +51 -0
  59. package/docs/releasing.md +5 -0
  60. package/docs/server-bun.md +5 -0
  61. package/docs/starter-agent-instructions.md +4 -2
  62. package/docs/worker.md +57 -0
  63. package/package.json +9 -5
  64. package/dist/chunks/contract-14vxdcrs.js.map +0 -10
  65. package/dist/chunks/contract-jksbmt5q.js.map +0 -10
  66. package/dist/chunks/contract-mb5nfzwg.js.map +0 -12
  67. package/dist/chunks/contract-tkkc28tg.js.map +0 -12
  68. package/dist/chunks/contract-tqrf3ykr.js.map +0 -11
  69. package/dist/types/embed/standalone.d.ts +0 -1
  70. package/dist/types/react/embed/shell.d.ts +0 -10
  71. package/docs/bootstrap.md +0 -76
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/server/handler.ts
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: "Lakehouse access needs attention. Run this app with 'altertable app dev', or run 'altertable login' and retry.",
40
- forbidden: "This app cannot access its lakehouse data. Check the selected profile's permissions.",
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 = operation.input(JSON.parse(body));
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 timeout = AbortSignal.timeout(operation.policy.maxDurationMs);
90
- const signal = AbortSignal.any([request.signal, timeout]);
91
- const queryIds = [];
92
- const queries = [];
93
- const boundedLakehouse = {
94
- async queryAll(statement, options) {
95
- if (!Number.isInteger(options.limit) || options.limit < 1)
96
- throw new Error("Query needs a positive row limit.");
97
- if (operation.queryNames && !Object.values(operation.queryNames).includes(options.name ?? ""))
98
- throw new Error(`Query name is not registered for operation ${name}.`);
99
- const query = {
100
- name: options.name ?? `Query ${queries.length + 1}`,
101
- statement
102
- };
103
- queries.push(query);
104
- let result;
105
- try {
106
- result = await access.lakehouse.queryAll(statement, {
107
- limit: Math.min(options.limit, operation.policy.maxQueryRows),
108
- signal: AbortSignal.any([signal, options.signal])
109
- });
110
- } catch (error) {
111
- if (error instanceof DataSourceError)
112
- error.queryName = query.name;
113
- throw error;
114
- }
115
- if (result.rows.length > operation.policy.maxQueryRows)
116
- throw new Error("Result exceeds row limit.");
117
- if (result.queryId) {
118
- query.queryId = result.queryId;
119
- queryIds.push(result.queryId);
120
- }
121
- return result;
122
- }
123
- };
124
- let abort;
246
+ const signal = request.signal;
125
247
  try {
126
- signal.throwIfAborted();
127
- const cancelled = new Promise((_, reject) => {
128
- function rejectAborted() {
129
- reject(new Error("Request aborted."));
130
- }
131
- abort = rejectAborted;
132
- signal.addEventListener("abort", rejectAborted, { once: true });
133
- if (signal.aborted)
134
- rejectAborted();
135
- });
136
- const data = operation.output(await Promise.race([
137
- cancelled,
138
- Promise.resolve().then(() => {
139
- signal.throwIfAborted();
140
- return operation.run({ lakehouse: boundedLakehouse, signal }, input);
141
- })
142
- ]));
143
- const result = {
144
- data,
248
+ const { serializedBody } = await executeDataOperation(operation, input, {
249
+ lakehouse: access.lakehouse,
250
+ includeSql: access.canDiscloseSql,
251
+ signal,
145
252
  requestId,
146
- queriedAt: new Date().toISOString(),
147
- queryIds,
148
- ...operation.policy.exposeSql && access.canDiscloseSql ? { queries } : {}
149
- };
150
- const body = JSON.stringify(result);
151
- if (new TextEncoder().encode(body).byteLength > (operation.policy.maxResponseBytes ?? 1e6)) {
152
- throw new Error("Response exceeds byte limit.");
153
- }
154
- return new Response(body, {
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
- if (source) {
164
- return problem(source.reason === "rate_limited" ? 429 : 502, `source_${source.reason}`, sourceErrorMessages[source.reason], requestId);
165
- }
166
- return problem(signal.aborted ? 504 : 502, signal.aborted ? "timeout" : "query_failed", signal.aborted ? "The data request timed out." : "The data request failed.", requestId);
167
- } finally {
168
- if (abort)
169
- signal.removeEventListener("abort", abort);
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) => JSON.parse(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: metadata.query_id };
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=FE9002DDE845605A64756E2164756E21
444
+ //# debugId=34D393952628512F64756E2164756E21