@databricks/appkit 0.1.5 → 0.2.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 (126) hide show
  1. package/AGENTS.md +52 -0
  2. package/CLAUDE.md +52 -0
  3. package/NOTICE.md +2 -0
  4. package/README.md +21 -15
  5. package/bin/appkit-lint.js +129 -0
  6. package/dist/analytics/analytics.d.ts.map +1 -1
  7. package/dist/analytics/analytics.js +16 -3
  8. package/dist/analytics/analytics.js.map +1 -1
  9. package/dist/analytics/query.js +8 -2
  10. package/dist/analytics/query.js.map +1 -1
  11. package/dist/app/index.d.ts.map +1 -1
  12. package/dist/app/index.js +7 -5
  13. package/dist/app/index.js.map +1 -1
  14. package/dist/appkit/package.js +1 -1
  15. package/dist/cache/index.d.ts.map +1 -1
  16. package/dist/cache/index.js +24 -3
  17. package/dist/cache/index.js.map +1 -1
  18. package/dist/cache/storage/persistent.js +12 -6
  19. package/dist/cache/storage/persistent.js.map +1 -1
  20. package/dist/connectors/lakebase/client.js +25 -14
  21. package/dist/connectors/lakebase/client.js.map +1 -1
  22. package/dist/connectors/sql-warehouse/client.js +68 -28
  23. package/dist/connectors/sql-warehouse/client.js.map +1 -1
  24. package/dist/context/service-context.js +13 -8
  25. package/dist/context/service-context.js.map +1 -1
  26. package/dist/errors/authentication.d.ts +38 -0
  27. package/dist/errors/authentication.d.ts.map +1 -0
  28. package/dist/errors/authentication.js +48 -0
  29. package/dist/errors/authentication.js.map +1 -0
  30. package/dist/errors/base.d.ts +58 -0
  31. package/dist/errors/base.d.ts.map +1 -0
  32. package/dist/errors/base.js +70 -0
  33. package/dist/errors/base.js.map +1 -0
  34. package/dist/errors/configuration.d.ts +38 -0
  35. package/dist/errors/configuration.d.ts.map +1 -0
  36. package/dist/errors/configuration.js +45 -0
  37. package/dist/errors/configuration.js.map +1 -0
  38. package/dist/errors/connection.d.ts +42 -0
  39. package/dist/errors/connection.d.ts.map +1 -0
  40. package/dist/errors/connection.js +54 -0
  41. package/dist/errors/connection.js.map +1 -0
  42. package/dist/errors/execution.d.ts +42 -0
  43. package/dist/errors/execution.d.ts.map +1 -0
  44. package/dist/errors/execution.js +51 -0
  45. package/dist/errors/execution.js.map +1 -0
  46. package/dist/errors/index.js +28 -0
  47. package/dist/errors/index.js.map +1 -0
  48. package/dist/errors/initialization.d.ts +34 -0
  49. package/dist/errors/initialization.d.ts.map +1 -0
  50. package/dist/errors/initialization.js +42 -0
  51. package/dist/errors/initialization.js.map +1 -0
  52. package/dist/errors/server.d.ts +38 -0
  53. package/dist/errors/server.d.ts.map +1 -0
  54. package/dist/errors/server.js +45 -0
  55. package/dist/errors/server.js.map +1 -0
  56. package/dist/errors/tunnel.d.ts +38 -0
  57. package/dist/errors/tunnel.d.ts.map +1 -0
  58. package/dist/errors/tunnel.js +51 -0
  59. package/dist/errors/tunnel.js.map +1 -0
  60. package/dist/errors/validation.d.ts +36 -0
  61. package/dist/errors/validation.d.ts.map +1 -0
  62. package/dist/errors/validation.js +45 -0
  63. package/dist/errors/validation.js.map +1 -0
  64. package/dist/index.d.ts +12 -3
  65. package/dist/index.js +18 -3
  66. package/dist/index.js.map +1 -0
  67. package/dist/logging/logger.js +179 -0
  68. package/dist/logging/logger.js.map +1 -0
  69. package/dist/logging/sampling.js +56 -0
  70. package/dist/logging/sampling.js.map +1 -0
  71. package/dist/logging/wide-event-emitter.js +108 -0
  72. package/dist/logging/wide-event-emitter.js.map +1 -0
  73. package/dist/logging/wide-event.js +167 -0
  74. package/dist/logging/wide-event.js.map +1 -0
  75. package/dist/plugin/dev-reader.d.ts.map +1 -1
  76. package/dist/plugin/dev-reader.js +8 -3
  77. package/dist/plugin/dev-reader.js.map +1 -1
  78. package/dist/plugin/interceptors/cache.js.map +1 -1
  79. package/dist/plugin/interceptors/retry.js +10 -2
  80. package/dist/plugin/interceptors/retry.js.map +1 -1
  81. package/dist/plugin/interceptors/telemetry.js +24 -9
  82. package/dist/plugin/interceptors/telemetry.js.map +1 -1
  83. package/dist/plugin/interceptors/timeout.js +4 -0
  84. package/dist/plugin/interceptors/timeout.js.map +1 -1
  85. package/dist/plugin/plugin.d.ts +1 -1
  86. package/dist/plugin/plugin.d.ts.map +1 -1
  87. package/dist/plugin/plugin.js +9 -4
  88. package/dist/plugin/plugin.js.map +1 -1
  89. package/dist/server/index.d.ts.map +1 -1
  90. package/dist/server/index.js +22 -17
  91. package/dist/server/index.js.map +1 -1
  92. package/dist/server/remote-tunnel/remote-tunnel-controller.js +4 -2
  93. package/dist/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
  94. package/dist/server/remote-tunnel/remote-tunnel-manager.js +10 -8
  95. package/dist/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
  96. package/dist/server/vite-dev-server.js +8 -3
  97. package/dist/server/vite-dev-server.js.map +1 -1
  98. package/dist/stream/arrow-stream-processor.js +13 -6
  99. package/dist/stream/arrow-stream-processor.js.map +1 -1
  100. package/dist/stream/buffers.js +5 -1
  101. package/dist/stream/buffers.js.map +1 -1
  102. package/dist/stream/stream-manager.d.ts.map +1 -1
  103. package/dist/stream/stream-manager.js +47 -36
  104. package/dist/stream/stream-manager.js.map +1 -1
  105. package/dist/stream/types.js.map +1 -1
  106. package/dist/telemetry/index.d.ts +2 -2
  107. package/dist/telemetry/index.js +2 -2
  108. package/dist/telemetry/instrumentations.js +14 -10
  109. package/dist/telemetry/instrumentations.js.map +1 -1
  110. package/dist/telemetry/telemetry-manager.js +8 -6
  111. package/dist/telemetry/telemetry-manager.js.map +1 -1
  112. package/dist/telemetry/trace-sampler.js +33 -0
  113. package/dist/telemetry/trace-sampler.js.map +1 -0
  114. package/dist/type-generator/index.js +4 -2
  115. package/dist/type-generator/index.js.map +1 -1
  116. package/dist/type-generator/query-registry.js +4 -2
  117. package/dist/type-generator/query-registry.js.map +1 -1
  118. package/dist/type-generator/vite-plugin.d.ts.map +1 -1
  119. package/dist/type-generator/vite-plugin.js +5 -3
  120. package/dist/type-generator/vite-plugin.js.map +1 -1
  121. package/dist/utils/env-validator.js +5 -1
  122. package/dist/utils/env-validator.js.map +1 -1
  123. package/dist/utils/path-exclusions.js +66 -0
  124. package/dist/utils/path-exclusions.js.map +1 -0
  125. package/llms.txt +52 -0
  126. package/package.json +4 -1
package/AGENTS.md CHANGED
@@ -668,6 +668,56 @@ export function SpendChart() {
668
668
  }
669
669
  ```
670
670
 
671
+ **Chart props reference (important):**
672
+
673
+ Charts are **self-contained ECharts components**. Configure via props, NOT children:
674
+
675
+ ```tsx
676
+ // ✅ Correct: use props for customization
677
+ <BarChart
678
+ queryKey="sales_by_region"
679
+ parameters={{}}
680
+ xKey="region" // X-axis field
681
+ yKey={["revenue", "expenses"]} // Y-axis field(s) - string or string[]
682
+ colors={['#40d1f5', '#4462c9']} // Custom colors
683
+ stacked // Stack bars (BarChart, AreaChart)
684
+ orientation="horizontal" // "vertical" (default) | "horizontal"
685
+ showLegend // Show legend
686
+ height={400} // Height in pixels (default: 300)
687
+ />
688
+
689
+ <LineChart
690
+ queryKey="trend_data"
691
+ parameters={{}}
692
+ xKey="date"
693
+ yKey="value"
694
+ smooth // Smooth curves (default: true)
695
+ showSymbol={false} // Hide data point markers
696
+ />
697
+ ```
698
+
699
+ **❌ CRITICAL: Charts do NOT accept Recharts children**
700
+
701
+ ```tsx
702
+ // ❌ WRONG - AppKit charts are NOT Recharts wrappers
703
+ import { BarChart } from "@databricks/appkit-ui/react";
704
+ import { Bar, XAxis, YAxis, CartesianGrid } from "recharts";
705
+
706
+ <BarChart queryKey="data" parameters={{}}>
707
+ <CartesianGrid /> // ❌ This will cause TypeScript errors
708
+ <XAxis dataKey="x" /> // ❌ Not supported
709
+ <Bar dataKey="y" /> // ❌ Not supported
710
+ </BarChart>
711
+
712
+ // ✅ CORRECT - use props instead
713
+ <BarChart
714
+ queryKey="data"
715
+ parameters={{}}
716
+ xKey="x"
717
+ yKey="y"
718
+ />
719
+ ```
720
+
671
721
  ### SQL helpers (`sql.*`)
672
722
 
673
723
  Use these to build typed parameters (they return marker objects: `{ __sql_type, value }`):
@@ -1169,6 +1219,7 @@ env:
1169
1219
  - `useMemo` wraps parameters objects
1170
1220
  - Loading/error/empty states are explicit
1171
1221
  - Charts use `format="auto"` unless you have a reason to force `"json"`/`"arrow"`
1222
+ - Charts use props (`xKey`, `yKey`, `colors`) NOT children (they're ECharts-based, not Recharts)
1172
1223
  - If using tooltips: root is wrapped with `<TooltipProvider>`
1173
1224
 
1174
1225
  - **Never**
@@ -1176,4 +1227,5 @@ env:
1176
1227
  - Don't pass untyped raw params for annotated queries
1177
1228
  - Don't ignore `createApp()`'s promise
1178
1229
  - Don't invent UI components not listed in this file
1230
+ - Don't pass Recharts children (`<Bar>`, `<XAxis>`, etc.) to AppKit chart components
1179
1231
 
package/CLAUDE.md CHANGED
@@ -668,6 +668,56 @@ export function SpendChart() {
668
668
  }
669
669
  ```
670
670
 
671
+ **Chart props reference (important):**
672
+
673
+ Charts are **self-contained ECharts components**. Configure via props, NOT children:
674
+
675
+ ```tsx
676
+ // ✅ Correct: use props for customization
677
+ <BarChart
678
+ queryKey="sales_by_region"
679
+ parameters={{}}
680
+ xKey="region" // X-axis field
681
+ yKey={["revenue", "expenses"]} // Y-axis field(s) - string or string[]
682
+ colors={['#40d1f5', '#4462c9']} // Custom colors
683
+ stacked // Stack bars (BarChart, AreaChart)
684
+ orientation="horizontal" // "vertical" (default) | "horizontal"
685
+ showLegend // Show legend
686
+ height={400} // Height in pixels (default: 300)
687
+ />
688
+
689
+ <LineChart
690
+ queryKey="trend_data"
691
+ parameters={{}}
692
+ xKey="date"
693
+ yKey="value"
694
+ smooth // Smooth curves (default: true)
695
+ showSymbol={false} // Hide data point markers
696
+ />
697
+ ```
698
+
699
+ **❌ CRITICAL: Charts do NOT accept Recharts children**
700
+
701
+ ```tsx
702
+ // ❌ WRONG - AppKit charts are NOT Recharts wrappers
703
+ import { BarChart } from "@databricks/appkit-ui/react";
704
+ import { Bar, XAxis, YAxis, CartesianGrid } from "recharts";
705
+
706
+ <BarChart queryKey="data" parameters={{}}>
707
+ <CartesianGrid /> // ❌ This will cause TypeScript errors
708
+ <XAxis dataKey="x" /> // ❌ Not supported
709
+ <Bar dataKey="y" /> // ❌ Not supported
710
+ </BarChart>
711
+
712
+ // ✅ CORRECT - use props instead
713
+ <BarChart
714
+ queryKey="data"
715
+ parameters={{}}
716
+ xKey="x"
717
+ yKey="y"
718
+ />
719
+ ```
720
+
671
721
  ### SQL helpers (`sql.*`)
672
722
 
673
723
  Use these to build typed parameters (they return marker objects: `{ __sql_type, value }`):
@@ -1169,6 +1219,7 @@ env:
1169
1219
  - `useMemo` wraps parameters objects
1170
1220
  - Loading/error/empty states are explicit
1171
1221
  - Charts use `format="auto"` unless you have a reason to force `"json"`/`"arrow"`
1222
+ - Charts use props (`xKey`, `yKey`, `colors`) NOT children (they're ECharts-based, not Recharts)
1172
1223
  - If using tooltips: root is wrapped with `<TooltipProvider>`
1173
1224
 
1174
1225
  - **Never**
@@ -1176,4 +1227,5 @@ env:
1176
1227
  - Don't pass untyped raw params for annotated queries
1177
1228
  - Don't ignore `createApp()`'s promise
1178
1229
  - Don't invent UI components not listed in this file
1230
+ - Don't pass Recharts children (`<Bar>`, `<XAxis>`, etc.) to AppKit chart components
1179
1231
 
package/NOTICE.md CHANGED
@@ -6,6 +6,7 @@ This Software contains code from the following open source projects:
6
6
 
7
7
  | Name | Installed version | License | Code |
8
8
  | :--------------- | :---------------- | :----------- | :--------------------------------------------------- |
9
+ | [@ast-grep/napi](https://www.npmjs.com/package/@ast-grep/napi) | 0.37.0 | MIT | https://ast-grep.github.io |
9
10
  | [@hookform/resolvers](https://www.npmjs.com/package/@hookform/resolvers) | 5.2.2 | MIT | https://react-hook-form.com |
10
11
  | [@opentelemetry/api](https://www.npmjs.com/package/@opentelemetry/api) | 1.9.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/api |
11
12
  | [@opentelemetry/api-logs](https://www.npmjs.com/package/@opentelemetry/api-logs) | 0.208.0 | Apache-2.0 | https://github.com/open-telemetry/opentelemetry-js/tree/main/experimental/packages/api-logs |
@@ -63,6 +64,7 @@ This Software contains code from the following open source projects:
63
64
  | [input-otp](https://www.npmjs.com/package/input-otp) | 1.4.2 | MIT | https://input-otp.rodz.dev/ |
64
65
  | [lucide-react](https://www.npmjs.com/package/lucide-react) | 0.554.0 | ISC | https://lucide.dev |
65
66
  | [next-themes](https://www.npmjs.com/package/next-themes) | 0.4.6 | MIT | https://github.com/pacocoursey/next-themes#readme |
67
+ | [obug](https://www.npmjs.com/package/obug) | 2.1.1 | MIT | https://github.com/sxzz/obug#readme |
66
68
  | [pg](https://www.npmjs.com/package/pg) | 8.16.3 | MIT | https://github.com/brianc/node-postgres |
67
69
  | [react-day-picker](https://www.npmjs.com/package/react-day-picker) | 9.12.0 | MIT | https://daypicker.dev |
68
70
  | [react-hook-form](https://www.npmjs.com/package/react-hook-form) | 7.68.0 | MIT | https://react-hook-form.com |
package/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # AppKit
2
2
 
3
- > [!WARNING]
4
- > ## ⚠️ PREVIEW - NOT FOR PRODUCTION USE
3
+ Build Databricks Apps faster with our brand-new Node.js + React SDK. Built for humans and AI.
4
+
5
+ > [!WARNING] PREVIEW - NOT FOR PRODUCTION USE
5
6
  >
6
7
  > **This SDK is in preview and is subject to change without notice.**
7
8
  >
@@ -11,25 +12,30 @@
11
12
  > - 📝 **Use for development and testing only**
12
13
  >
13
14
 
14
- ## Contributing
15
+ ## Introduction
15
16
 
16
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution guidelines.
17
+ AppKit is a TypeScript SDK for building production-ready Databricks applications with a plugin-based architecture. It provides opinionated defaults, built-in observability, and seamless integration with Databricks services.
17
18
 
18
- ## Documentation
19
+ AppKit simplifies building data applications on Databricks by providing:
20
+
21
+ - **Plugin architecture**: Modular design with built-in server and analytics plugins
22
+ - **Type safety**: End-to-end TypeScript with automatic query type generation
23
+ - **Production-ready features**: Built-in caching, telemetry, retry logic, and error handling
24
+ - **Developer experience**: Remote hot reload, file-based queries, optimized for AI-assisted development
25
+ - **Databricks native**: Seamless integration with SQL Warehouses, Unity Catalog, and other workspace resources
19
26
 
20
- The `docs/` directory contains the AppKit documentation site, built with Docusaurus.
27
+ ## Getting started
21
28
 
22
- **Working with docs:**
29
+ Follow the [Getting Started](https://databricks.github.io/appkit/docs/) guide to get started with AppKit.
23
30
 
24
- ```bash
25
- # From root
26
- pnpm docs:dev # Start dev server
27
- pnpm docs:build # Build docs
28
- pnpm docs:serve # Serve built docs
29
- ```
31
+ ## Documentation
30
32
 
31
- See [docs/README.md](./docs/README.md) for more details.
33
+ 📖 For full AppKit documentation, visit the [AppKit Documentation](https://databricks.github.io/appkit/) website.
32
34
 
33
35
  👉 For AI/code assistants:
34
36
  - Use [llms-compact.txt](./llms-compact.txt) for quick usage patterns.
35
- - See [llms.txt](./llms.txt) for full guidance and anti-patterns.
37
+ - See [llms.txt](./llms.txt) for full guidance and anti-patterns.
38
+
39
+ ## Contributing
40
+
41
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and contribution guidelines.
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * AST-based linting using ast-grep.
4
+ * Catches patterns that ESLint/TypeScript miss or handle poorly.
5
+ * Usage: npx appkit-lint
6
+ */
7
+ import { parse, Lang } from "@ast-grep/napi";
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+
11
+ const rules = [
12
+ {
13
+ id: "no-double-type-assertion",
14
+ pattern: "$X as unknown as $Y",
15
+ message:
16
+ "Avoid double type assertion (as unknown as). Use proper type guards or fix the source type.",
17
+ },
18
+ {
19
+ id: "no-as-any",
20
+ pattern: "$X as any",
21
+ message:
22
+ 'Avoid "as any" type assertion. Use proper typing or unknown with type guards.',
23
+ includeTests: false, // acceptable in test mocks
24
+ },
25
+ {
26
+ id: "no-array-index-key",
27
+ pattern: "key={$IDX}",
28
+ message:
29
+ "Avoid using array index as React key. Use a stable unique identifier.",
30
+ filter: (code) => /key=\{(idx|index|i)\}/.test(code),
31
+ },
32
+ {
33
+ id: "no-parse-float-without-validation",
34
+ pattern: "parseFloat($X).toFixed($Y)",
35
+ message:
36
+ "parseFloat can return NaN. Validate input or use toNumber() helper from shared/types.ts.",
37
+ },
38
+ ];
39
+
40
+ function isTestFile(filePath) {
41
+ return (
42
+ /\.(test|spec)\.(ts|tsx)$/.test(filePath) || filePath.includes("/tests/")
43
+ );
44
+ }
45
+
46
+ function findTsFiles(dir, files = []) {
47
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
48
+
49
+ for (const entry of entries) {
50
+ const fullPath = path.join(dir, entry.name);
51
+
52
+ if (entry.isDirectory()) {
53
+ if (["node_modules", "dist", "build", ".git"].includes(entry.name))
54
+ continue;
55
+ findTsFiles(fullPath, files);
56
+ } else if (entry.isFile() && /\.(ts|tsx)$/.test(entry.name)) {
57
+ files.push(fullPath);
58
+ }
59
+ }
60
+
61
+ return files;
62
+ }
63
+
64
+ function lintFile(filePath, rules) {
65
+ const violations = [];
66
+ const content = fs.readFileSync(filePath, "utf-8");
67
+ const lang = filePath.endsWith(".tsx") ? Lang.Tsx : Lang.TypeScript;
68
+ const testFile = isTestFile(filePath);
69
+
70
+ const ast = parse(lang, content);
71
+ const root = ast.root();
72
+
73
+ for (const rule of rules) {
74
+ // skip rules that don't apply to test files
75
+ if (testFile && rule.includeTests === false) continue;
76
+
77
+ const matches = root.findAll(rule.pattern);
78
+
79
+ for (const match of matches) {
80
+ const code = match.text();
81
+
82
+ if (rule.filter && !rule.filter(code)) continue;
83
+
84
+ const range = match.range();
85
+ violations.push({
86
+ file: filePath,
87
+ line: range.start.line + 1,
88
+ column: range.start.column + 1,
89
+ rule: rule.id,
90
+ message: rule.message,
91
+ code: code.length > 80 ? `${code.slice(0, 77)}...` : code,
92
+ });
93
+ }
94
+ }
95
+
96
+ return violations;
97
+ }
98
+
99
+ function main() {
100
+ const rootDir = process.cwd();
101
+ const files = findTsFiles(rootDir);
102
+
103
+ console.log(`Scanning ${files.length} TypeScript files...\n`);
104
+
105
+ const allViolations = [];
106
+
107
+ for (const file of files) {
108
+ const violations = lintFile(file, rules);
109
+ allViolations.push(...violations);
110
+ }
111
+
112
+ if (allViolations.length === 0) {
113
+ console.log("No ast-grep lint violations found.");
114
+ process.exit(0);
115
+ }
116
+
117
+ console.log(`Found ${allViolations.length} violation(s):\n`);
118
+
119
+ for (const v of allViolations) {
120
+ const relPath = path.relative(rootDir, v.file);
121
+ console.log(`${relPath}:${v.line}:${v.column}`);
122
+ console.log(` ${v.rule}: ${v.message}`);
123
+ console.log(` > ${v.code}\n`);
124
+ }
125
+
126
+ process.exit(1);
127
+ }
128
+
129
+ main();
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../src/analytics/analytics.ts"],"sourcesContent":[],"mappings":";;;;;;;;cAuBa,eAAA,SAAwB,MAAA;;;EAAxB,iBAAA,WAAgB,EAAA,MAAA;EAAA,UAAA,MAAA,EAKD,gBALC;UAKD,SAAA;UAMN,cAAA;aAWC,CAAA,MAAA,EAXD,gBAWC;cA6CN,CAAA,MAAA,EA7CM,UA6CN,CAAA,EAAA,IAAA;;;;;mBAqCZ,CAAA,GAAA,EArCI,OAAA,CAAQ,OAqCZ,EAAA,GAAA,EApCI,OAAA,CAAQ,QAoCZ,CAAA,EAnCA,OAmCA,CAAA,IAAA,CAAA;;;;;mBAkGA,CAAA,GAAA,EApGI,OAAA,CAAQ,OAoGZ,EAAA,GAAA,EAnGI,OAAA,CAAQ,QAmGZ,CAAA,EAlGA,OAkGA,CAAA,IAAA,CAAA;;;;;;;;AAwCL;;;;;;;;oCA3CiB,eAAe,sDACT,8BACV,cACR;;;;0CAyBgB,yCAER,cACR,QAAQ;cAIO;;;;;cAQP,WAAS,gBAAA,iBAAA"}
1
+ {"version":3,"file":"analytics.d.ts","names":[],"sources":["../../src/analytics/analytics.ts"],"sourcesContent":[],"mappings":";;;;;;;;cA0Ba,eAAA,SAAwB,MAAA;;;EAAxB,iBAAA,WAAgB,EAAA,MAAA;EAAA,UAAA,MAAA,EAKD,gBALC;UAKD,SAAA;UAMN,cAAA;aAWC,CAAA,MAAA,EAXD,gBAWC;cA6CN,CAAA,MAAA,EA7CM,UA6CN,CAAA,EAAA,IAAA;;;;;mBA2CZ,CAAA,GAAA,EA3CI,OAAA,CAAQ,OA2CZ,EAAA,GAAA,EA1CI,OAAA,CAAQ,QA0CZ,CAAA,EAzCA,OAyCA,CAAA,IAAA,CAAA;;;;;mBA8GA,CAAA,GAAA,EAhHI,OAAA,CAAQ,OAgHZ,EAAA,GAAA,EA/GI,OAAA,CAAQ,QA+GZ,CAAA,EA9GA,OA8GA,CAAA,IAAA,CAAA;;;;;;;;AAwCL;;;;;;;;oCA3CiB,eAAe,sDACT,8BACV,cACR;;;;0CAyBgB,yCAER,cACR,QAAQ;cAIO;;;;;cAQP,WAAS,gBAAA,iBAAA"}
@@ -1,3 +1,4 @@
1
+ import { createLogger } from "../logging/logger.js";
1
2
  import { getCurrentUserId, getWarehouseId, getWorkspaceClient } from "../context/execution-context.js";
2
3
  import { init_context } from "../context/index.js";
3
4
  import { SQLWarehouseConnector } from "../connectors/sql-warehouse/client.js";
@@ -10,6 +11,7 @@ import { QueryProcessor } from "./query.js";
10
11
 
11
12
  //#region src/analytics/analytics.ts
12
13
  init_context();
14
+ const logger = createLogger("analytics");
13
15
  var AnalyticsPlugin = class extends Plugin {
14
16
  static {
15
17
  this.description = "Analytics plugin for data analysis";
@@ -67,15 +69,19 @@ var AnalyticsPlugin = class extends Plugin {
67
69
  try {
68
70
  const { jobId } = req.params;
69
71
  const workspaceClient = getWorkspaceClient();
70
- console.log(`Processing Arrow job request: ${jobId} for plugin: ${this.name}`);
72
+ logger.debug("Processing Arrow job request for jobId=%s", jobId);
73
+ logger.event(req)?.setComponent("analytics", "getArrowData").setContext("analytics", {
74
+ job_id: jobId,
75
+ plugin: this.name
76
+ });
71
77
  const result = await this.getArrowData(workspaceClient, jobId);
72
78
  res.setHeader("Content-Type", "application/octet-stream");
73
79
  res.setHeader("Content-Length", result.data.length.toString());
74
80
  res.setHeader("Cache-Control", "public, max-age=3600");
75
- console.log(`Sending Arrow buffer: ${result.data.length} bytes for job ${jobId}`);
81
+ logger.debug("Sending Arrow buffer: %d bytes for job %s", result.data.length, jobId);
76
82
  res.send(Buffer.from(result.data));
77
83
  } catch (error) {
78
- console.error(`Arrow job error for ${this.name}:`, error);
84
+ logger.error("Arrow job error: %O", error);
79
85
  res.status(404).json({
80
86
  error: error instanceof Error ? error.message : "Arrow job not found",
81
87
  plugin: this.name
@@ -89,6 +95,13 @@ var AnalyticsPlugin = class extends Plugin {
89
95
  async _handleQueryRoute(req, res) {
90
96
  const { query_key } = req.params;
91
97
  const { parameters, format = "JSON" } = req.body;
98
+ logger.debug(req, "Executing query: %s (format=%s)", query_key, format);
99
+ logger.event(req)?.setComponent("analytics", "executeQuery").setContext("analytics", {
100
+ query_key,
101
+ format,
102
+ parameter_count: parameters ? Object.keys(parameters).length : 0,
103
+ plugin: this.name
104
+ });
92
105
  const queryParameters = format === "ARROW" ? {
93
106
  formatParameters: {
94
107
  disposition: "EXTERNAL_LINKS",
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","names":[],"sources":["../../src/analytics/analytics.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type {\n IAppRouter,\n PluginExecuteConfig,\n SQLTypeMarker,\n StreamExecutionSettings,\n} from \"shared\";\nimport { SQLWarehouseConnector } from \"../connectors\";\nimport {\n getCurrentUserId,\n getWarehouseId,\n getWorkspaceClient,\n} from \"../context\";\nimport type express from \"express\";\nimport { Plugin, toPlugin } from \"../plugin\";\nimport { queryDefaults } from \"./defaults\";\nimport { QueryProcessor } from \"./query\";\nimport type {\n AnalyticsQueryResponse,\n IAnalyticsConfig,\n IAnalyticsQueryRequest,\n} from \"./types\";\n\nexport class AnalyticsPlugin extends Plugin {\n name = \"analytics\";\n envVars = [];\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n // Service principal endpoints\n this.route(router, {\n name: \"arrow\",\n method: \"get\",\n path: \"/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // User context endpoints - use asUser(req) to execute with user's identity\n this.route(router, {\n name: \"arrowAsUser\",\n method: \"get\",\n path: \"/users/me/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"queryAsUser\",\n method: \"post\",\n path: \"/users/me/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleQueryRoute(req, res);\n },\n });\n }\n\n /**\n * Handle Arrow data download requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleArrowRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n try {\n const { jobId } = req.params;\n const workspaceClient = getWorkspaceClient();\n\n console.log(\n `Processing Arrow job request: ${jobId} for plugin: ${this.name}`,\n );\n\n const result = await this.getArrowData(workspaceClient, jobId);\n\n res.setHeader(\"Content-Type\", \"application/octet-stream\");\n res.setHeader(\"Content-Length\", result.data.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n console.log(\n `Sending Arrow buffer: ${result.data.length} bytes for job ${jobId}`,\n );\n res.send(Buffer.from(result.data));\n } catch (error) {\n console.error(`Arrow job error for ${this.name}:`, error);\n res.status(404).json({\n error: error instanceof Error ? error.message : \"Arrow job not found\",\n plugin: this.name,\n });\n }\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format = \"JSON\" } = req.body as IAnalyticsQueryRequest;\n const queryParameters =\n format === \"ARROW\"\n ? {\n formatParameters: {\n disposition: \"EXTERNAL_LINKS\",\n format: \"ARROW_STREAM\",\n },\n type: \"arrow\",\n }\n : {\n type: \"result\",\n };\n\n // Get user key from current context (automatically includes user ID when in user context)\n const userKey = getCurrentUserId();\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const query = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!query) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const defaultConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n JSON.stringify(format),\n hashedQuery,\n userKey,\n ],\n },\n };\n\n const streamExecutionSettings: StreamExecutionSettings = {\n default: defaultConfig,\n };\n\n await this.executeStream(\n res,\n async (signal) => {\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const result = await this.query(\n query,\n processedParams,\n queryParameters.formatParameters,\n signal,\n );\n\n return { type: queryParameters.type, ...result };\n },\n streamExecutionSettings,\n userKey,\n );\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n /**\n * Get Arrow-formatted data for a completed query job.\n */\n protected async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.SQLClient.getArrowData>> {\n return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n}\n\n/**\n * @internal\n */\nexport const analytics = toPlugin<\n typeof AnalyticsPlugin,\n IAnalyticsConfig,\n \"analytics\"\n>(AnalyticsPlugin, \"analytics\");\n"],"mappings":"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"}
1
+ {"version":3,"file":"analytics.js","names":[],"sources":["../../src/analytics/analytics.ts"],"sourcesContent":["import type { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRouter,\n PluginExecuteConfig,\n SQLTypeMarker,\n StreamExecutionSettings,\n} from \"shared\";\nimport { SQLWarehouseConnector } from \"../connectors\";\nimport {\n getCurrentUserId,\n getWarehouseId,\n getWorkspaceClient,\n} from \"../context\";\nimport { createLogger } from \"../logging/logger\";\nimport { Plugin, toPlugin } from \"../plugin\";\nimport { queryDefaults } from \"./defaults\";\nimport { QueryProcessor } from \"./query\";\nimport type {\n AnalyticsQueryResponse,\n IAnalyticsConfig,\n IAnalyticsQueryRequest,\n} from \"./types\";\n\nconst logger = createLogger(\"analytics\");\n\nexport class AnalyticsPlugin extends Plugin {\n name = \"analytics\";\n envVars = [];\n\n protected static description = \"Analytics plugin for data analysis\";\n protected declare config: IAnalyticsConfig;\n\n // analytics services\n private SQLClient: SQLWarehouseConnector;\n private queryProcessor: QueryProcessor;\n\n constructor(config: IAnalyticsConfig) {\n super(config);\n this.config = config;\n this.queryProcessor = new QueryProcessor();\n\n this.SQLClient = new SQLWarehouseConnector({\n timeout: config.timeout,\n telemetry: config.telemetry,\n });\n }\n\n injectRoutes(router: IAppRouter) {\n // Service principal endpoints\n this.route(router, {\n name: \"arrow\",\n method: \"get\",\n path: \"/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"query\",\n method: \"post\",\n path: \"/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this._handleQueryRoute(req, res);\n },\n });\n\n // User context endpoints - use asUser(req) to execute with user's identity\n this.route(router, {\n name: \"arrowAsUser\",\n method: \"get\",\n path: \"/users/me/arrow-result/:jobId\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleArrowRoute(req, res);\n },\n });\n\n this.route<AnalyticsQueryResponse>(router, {\n name: \"queryAsUser\",\n method: \"post\",\n path: \"/users/me/query/:query_key\",\n handler: async (req: express.Request, res: express.Response) => {\n await this.asUser(req)._handleQueryRoute(req, res);\n },\n });\n }\n\n /**\n * Handle Arrow data download requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleArrowRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n try {\n const { jobId } = req.params;\n const workspaceClient = getWorkspaceClient();\n\n logger.debug(\"Processing Arrow job request for jobId=%s\", jobId);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"getArrowData\").setContext(\"analytics\", {\n job_id: jobId,\n plugin: this.name,\n });\n\n const result = await this.getArrowData(workspaceClient, jobId);\n\n res.setHeader(\"Content-Type\", \"application/octet-stream\");\n res.setHeader(\"Content-Length\", result.data.length.toString());\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n logger.debug(\n \"Sending Arrow buffer: %d bytes for job %s\",\n result.data.length,\n jobId,\n );\n res.send(Buffer.from(result.data));\n } catch (error) {\n logger.error(\"Arrow job error: %O\", error);\n res.status(404).json({\n error: error instanceof Error ? error.message : \"Arrow job not found\",\n plugin: this.name,\n });\n }\n }\n\n /**\n * Handle SQL query execution requests.\n * When called via asUser(req), uses the user's Databricks credentials.\n */\n async _handleQueryRoute(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { query_key } = req.params;\n const { parameters, format = \"JSON\" } = req.body as IAnalyticsQueryRequest;\n\n // Request-scoped logging with WideEvent tracking\n logger.debug(req, \"Executing query: %s (format=%s)\", query_key, format);\n\n const event = logger.event(req);\n event?.setComponent(\"analytics\", \"executeQuery\").setContext(\"analytics\", {\n query_key,\n format,\n parameter_count: parameters ? Object.keys(parameters).length : 0,\n plugin: this.name,\n });\n\n const queryParameters =\n format === \"ARROW\"\n ? {\n formatParameters: {\n disposition: \"EXTERNAL_LINKS\",\n format: \"ARROW_STREAM\",\n },\n type: \"arrow\",\n }\n : {\n type: \"result\",\n };\n\n // Get user key from current context (automatically includes user ID when in user context)\n const userKey = getCurrentUserId();\n\n if (!query_key) {\n res.status(400).json({ error: \"query_key is required\" });\n return;\n }\n\n const query = await this.app.getAppQuery(\n query_key,\n req,\n this.devFileReader,\n );\n\n if (!query) {\n res.status(404).json({ error: \"Query not found\" });\n return;\n }\n\n const hashedQuery = this.queryProcessor.hashQuery(query);\n\n const defaultConfig: PluginExecuteConfig = {\n ...queryDefaults,\n cache: {\n ...queryDefaults.cache,\n cacheKey: [\n \"analytics:query\",\n query_key,\n JSON.stringify(parameters),\n JSON.stringify(format),\n hashedQuery,\n userKey,\n ],\n },\n };\n\n const streamExecutionSettings: StreamExecutionSettings = {\n default: defaultConfig,\n };\n\n await this.executeStream(\n res,\n async (signal) => {\n const processedParams = await this.queryProcessor.processQueryParams(\n query,\n parameters,\n );\n\n const result = await this.query(\n query,\n processedParams,\n queryParameters.formatParameters,\n signal,\n );\n\n return { type: queryParameters.type, ...result };\n },\n streamExecutionSettings,\n userKey,\n );\n }\n\n /**\n * Execute a SQL query using the current execution context.\n *\n * When called directly: uses service principal credentials.\n * When called via asUser(req).query(...): uses user's credentials.\n *\n * @example\n * ```typescript\n * // Service principal execution\n * const result = await analytics.query(\"SELECT * FROM table\")\n *\n * // User context execution (in route handler)\n * const result = await this.asUser(req).query(\"SELECT * FROM table\")\n * ```\n */\n async query(\n query: string,\n parameters?: Record<string, SQLTypeMarker | null | undefined>,\n formatParameters?: Record<string, any>,\n signal?: AbortSignal,\n ): Promise<any> {\n const workspaceClient = getWorkspaceClient();\n const warehouseId = await getWarehouseId();\n\n const { statement, parameters: sqlParameters } =\n this.queryProcessor.convertToSQLParameters(query, parameters);\n\n const response = await this.SQLClient.executeStatement(\n workspaceClient,\n {\n statement,\n warehouse_id: warehouseId,\n parameters: sqlParameters,\n ...formatParameters,\n },\n signal,\n );\n\n return response.result;\n }\n\n /**\n * Get Arrow-formatted data for a completed query job.\n */\n protected async getArrowData(\n workspaceClient: WorkspaceClient,\n jobId: string,\n signal?: AbortSignal,\n ): Promise<ReturnType<typeof this.SQLClient.getArrowData>> {\n return await this.SQLClient.getArrowData(workspaceClient, jobId, signal);\n }\n\n async shutdown(): Promise<void> {\n this.streamManager.abortAll();\n }\n}\n\n/**\n * @internal\n */\nexport const analytics = toPlugin<\n typeof AnalyticsPlugin,\n IAnalyticsConfig,\n \"analytics\"\n>(AnalyticsPlugin, \"analytics\");\n"],"mappings":"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"}
@@ -1,10 +1,13 @@
1
1
  import { isSQLTypeMarker, sql } from "../shared/src/sql/helpers.js";
2
+ import { ValidationError } from "../errors/validation.js";
3
+ import { init_errors } from "../errors/index.js";
2
4
  import { getWorkspaceId } from "../context/execution-context.js";
3
5
  import { init_context } from "../context/index.js";
4
6
  import { createHash } from "node:crypto";
5
7
 
6
8
  //#region src/analytics/query.ts
7
9
  init_context();
10
+ init_errors();
8
11
  var QueryProcessor = class {
9
12
  async processQueryParams(query, parameters) {
10
13
  const processed = { ...parameters };
@@ -23,7 +26,10 @@ var QueryProcessor = class {
23
26
  if (parameters) {
24
27
  const queryParamMatches = query.matchAll(/:([a-zA-Z_]\w*)/g);
25
28
  const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));
26
- for (const key of Object.keys(parameters)) if (!queryParams.has(key)) throw new Error(`Parameter "${key}" not found in query. Valid parameters: ${Array.from(queryParams).join(", ") || "none"}`);
29
+ for (const key of Object.keys(parameters)) if (!queryParams.has(key)) {
30
+ const validParams = Array.from(queryParams).join(", ") || "none";
31
+ throw ValidationError.invalidValue(key, parameters[key], `a parameter defined in the query (valid: ${validParams})`);
32
+ }
27
33
  for (const [key, value] of Object.entries(parameters)) {
28
34
  const parameter = this._createParameter(key, value);
29
35
  if (parameter) sqlParameters.push(parameter);
@@ -36,7 +42,7 @@ var QueryProcessor = class {
36
42
  }
37
43
  _createParameter(key, value) {
38
44
  if (value === null || value === void 0) return null;
39
- if (!isSQLTypeMarker(value)) throw new Error(`Parameter "${key}" must be a SQL type. Use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean().`);
45
+ if (!isSQLTypeMarker(value)) throw ValidationError.invalidValue(key, value, "SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())");
40
46
  return {
41
47
  name: key,
42
48
  value: value.value,
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../src/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { sql } from \"@databricks/sdk-experimental\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../context\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n throw new Error(\n `Parameter \"${key}\" not found in query. Valid parameters: ${\n Array.from(queryParams).join(\", \") || \"none\"\n }`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw new Error(\n `Parameter \"${key}\" must be a SQL type. Use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean().`,\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;cAG4C;AAI5C,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,CACvB,OAAM,IAAI,MACR,cAAc,IAAI,0CAChB,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI,SAEzC;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,IAAI,MACR,cAAc,IAAI,sGACnB;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
1
+ {"version":3,"file":"query.js","names":["sqlHelpers"],"sources":["../../src/analytics/query.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { sql } from \"@databricks/sdk-experimental\";\nimport { isSQLTypeMarker, type SQLTypeMarker, sql as sqlHelpers } from \"shared\";\nimport { getWorkspaceId } from \"../context\";\nimport { ValidationError } from \"../errors\";\n\ntype SQLParameterValue = SQLTypeMarker | null | undefined;\n\nexport class QueryProcessor {\n async processQueryParams(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): Promise<Record<string, SQLParameterValue>> {\n const processed = { ...parameters };\n\n // extract all params from the query\n const paramMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(paramMatches, (m) => m[1]));\n\n // auto-inject workspaceId if needed and not provided\n if (queryParams.has(\"workspaceId\") && !processed.workspaceId) {\n const workspaceId = await getWorkspaceId();\n if (workspaceId) {\n processed.workspaceId = sqlHelpers.string(workspaceId);\n }\n }\n\n return processed;\n }\n\n hashQuery(query: string): string {\n return createHash(\"md5\").update(query).digest(\"hex\");\n }\n\n convertToSQLParameters(\n query: string,\n parameters?: Record<string, SQLParameterValue>,\n ): { statement: string; parameters: sql.StatementParameterListItem[] } {\n const sqlParameters: sql.StatementParameterListItem[] = [];\n\n if (parameters) {\n // extract all params from the query\n const queryParamMatches = query.matchAll(/:([a-zA-Z_]\\w*)/g);\n const queryParams = new Set(Array.from(queryParamMatches, (m) => m[1]));\n\n // only allow parameters that exist in the query\n for (const key of Object.keys(parameters)) {\n if (!queryParams.has(key)) {\n const validParams = Array.from(queryParams).join(\", \") || \"none\";\n throw ValidationError.invalidValue(\n key,\n parameters[key],\n `a parameter defined in the query (valid: ${validParams})`,\n );\n }\n }\n\n // convert parameters to SQL parameters\n for (const [key, value] of Object.entries(parameters)) {\n const parameter = this._createParameter(key, value);\n if (parameter) {\n sqlParameters.push(parameter);\n }\n }\n }\n\n return { statement: query, parameters: sqlParameters };\n }\n\n private _createParameter(\n key: string,\n value: SQLParameterValue,\n ): sql.StatementParameterListItem | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n if (!isSQLTypeMarker(value)) {\n throw ValidationError.invalidValue(\n key,\n value,\n \"SQL type (use sql.string(), sql.number(), sql.date(), sql.timestamp(), or sql.boolean())\",\n );\n }\n\n return {\n name: key,\n value: value.value,\n type: value.__sql_type,\n };\n }\n}\n"],"mappings":";;;;;;;;cAG4C;aACA;AAI5C,IAAa,iBAAb,MAA4B;CAC1B,MAAM,mBACJ,OACA,YAC4C;EAC5C,MAAM,YAAY,EAAE,GAAG,YAAY;EAGnC,MAAM,eAAe,MAAM,SAAS,mBAAmB;AAIvD,MAHoB,IAAI,IAAI,MAAM,KAAK,eAAe,MAAM,EAAE,GAAG,CAAC,CAGlD,IAAI,cAAc,IAAI,CAAC,UAAU,aAAa;GAC5D,MAAM,cAAc,MAAM,gBAAgB;AAC1C,OAAI,YACF,WAAU,cAAcA,IAAW,OAAO,YAAY;;AAI1D,SAAO;;CAGT,UAAU,OAAuB;AAC/B,SAAO,WAAW,MAAM,CAAC,OAAO,MAAM,CAAC,OAAO,MAAM;;CAGtD,uBACE,OACA,YACqE;EACrE,MAAM,gBAAkD,EAAE;AAE1D,MAAI,YAAY;GAEd,MAAM,oBAAoB,MAAM,SAAS,mBAAmB;GAC5D,MAAM,cAAc,IAAI,IAAI,MAAM,KAAK,oBAAoB,MAAM,EAAE,GAAG,CAAC;AAGvE,QAAK,MAAM,OAAO,OAAO,KAAK,WAAW,CACvC,KAAI,CAAC,YAAY,IAAI,IAAI,EAAE;IACzB,MAAM,cAAc,MAAM,KAAK,YAAY,CAAC,KAAK,KAAK,IAAI;AAC1D,UAAM,gBAAgB,aACpB,KACA,WAAW,MACX,4CAA4C,YAAY,GACzD;;AAKL,QAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW,EAAE;IACrD,MAAM,YAAY,KAAK,iBAAiB,KAAK,MAAM;AACnD,QAAI,UACF,eAAc,KAAK,UAAU;;;AAKnC,SAAO;GAAE,WAAW;GAAO,YAAY;GAAe;;CAGxD,AAAQ,iBACN,KACA,OACuC;AACvC,MAAI,UAAU,QAAQ,UAAU,OAC9B,QAAO;AAGT,MAAI,CAAC,gBAAgB,MAAM,CACzB,OAAM,gBAAgB,aACpB,KACA,OACA,2FACD;AAGH,SAAO;GACL,MAAM;GACN,OAAO,MAAM;GACb,MAAM,MAAM;GACb"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":[],"mappings":";UAGU,WAAA;EAAA,KAAA,CAAA,EACA,MADW,CAAA,MAAA,EAAA,GAAA,CAAA;EAAA,OAAA,EAEV,MAFU,CAAA,MAAA,EAAA,MAAA,GAAA,MAAA,EAAA,GAAA,SAAA,CAAA;;UAKX,aAAA,CAHC;EAAM,QAAA,CAAA,QAAA,EAAA,MAAA,EAAA,GAAA,EAIiB,WAJjB,CAAA,EAI+B,OAJ/B,CAAA,MAAA,CAAA;AAAA;AAGM,cAIV,UAAA,CAJU;;;;AAIvB;;;;;;sCAYU,6BACU,gBACf"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":[],"mappings":";UAMU,WAAA;EAAA,KAAA,CAAA,EACA,MADW,CAAA,MAAA,EAAA,GAAA,CAAA;EAAA,OAAA,EAEV,MAFU,CAAA,MAAA,EAAA,MAAA,GAAA,MAAA,EAAA,GAAA,SAAA,CAAA;;UAKX,aAAA,CAHC;EAAM,QAAA,CAAA,QAAA,EAAA,MAAA,EAAA,GAAA,EAIiB,WAJjB,CAAA,EAI+B,OAJ/B,CAAA,MAAA,CAAA;AAAA;AAGM,cAIV,UAAA,CAJU;;;;AAIvB;;;;;;sCAYU,6BACU,gBACf"}
package/dist/app/index.js CHANGED
@@ -1,7 +1,9 @@
1
+ import { createLogger } from "../logging/logger.js";
1
2
  import fs from "node:fs/promises";
2
3
  import path from "node:path";
3
4
 
4
5
  //#region src/app/index.ts
6
+ const logger = createLogger("app");
5
7
  var AppManager = class {
6
8
  /**
7
9
  * Retrieves a query file by key from the queries directory
@@ -14,31 +16,31 @@ var AppManager = class {
14
16
  */
15
17
  async getAppQuery(queryKey, req, devFileReader) {
16
18
  if (!queryKey || !/^[a-zA-Z0-9_-]+$/.test(queryKey)) {
17
- console.error(`Invalid query key format: "${queryKey}". Only alphanumeric characters, underscores, and hyphens are allowed.`);
19
+ logger.error("Invalid query key format: %s. Only alphanumeric characters, underscores, and hyphens are allowed.", queryKey);
18
20
  return null;
19
21
  }
20
22
  const queryFilePath = path.join(process.cwd(), "config/queries", `${queryKey}.sql`);
21
23
  const resolvedPath = path.resolve(queryFilePath);
22
24
  const queriesDir = path.resolve(process.cwd(), "config/queries");
23
25
  if (!resolvedPath.startsWith(queriesDir)) {
24
- console.error(`Invalid query path: path traversal detected`);
26
+ logger.error("Invalid query path: path traversal detected");
25
27
  return null;
26
28
  }
27
29
  if (req?.query?.dev !== void 0 && devFileReader && req) try {
28
30
  const relativePath = path.relative(process.cwd(), resolvedPath);
29
31
  return await devFileReader.readFile(relativePath, req);
30
32
  } catch (error) {
31
- console.error(`Failed to read query "${queryKey}" from dev tunnel: ${error.message}`);
33
+ logger.error("Failed to read query %s from dev tunnel: %s", queryKey, error.message);
32
34
  return null;
33
35
  }
34
36
  try {
35
37
  return await fs.readFile(resolvedPath, "utf8");
36
38
  } catch (error) {
37
39
  if (error.code === "ENOENT") {
38
- console.error(`Query "${queryKey}" not found at path: ${resolvedPath}`);
40
+ logger.debug("Query %s not found at path: %s", queryKey, resolvedPath);
39
41
  return null;
40
42
  }
41
- console.error(`Failed to read query "${queryKey}" from server filesystem: ${error.message}`);
43
+ logger.error("Failed to read query %s from server filesystem: %s", queryKey, error.message);
42
44
  return null;
43
45
  }
44
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\ninterface RequestLike {\n query?: Record<string, any>;\n headers: Record<string, string | string[] | undefined>;\n}\n\ninterface DevFileReader {\n readFile(filePath: string, req: RequestLike): Promise<string>;\n}\n\nexport class AppManager {\n /**\n * Retrieves a query file by key from the queries directory\n * In dev mode with a request context, reads from local filesystem via WebSocket\n * @param queryKey - The query file name (without extension)\n * @param req - Optional request object to detect dev mode\n * @param devFileReader - Optional DevFileReader instance to read files from local filesystem\n * @returns The query content as a string\n * @throws Error if query key is invalid or file not found\n */\n async getAppQuery(\n queryKey: string,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n ): Promise<string | null> {\n // Security: Sanitize query key to prevent path traversal\n if (!queryKey || !/^[a-zA-Z0-9_-]+$/.test(queryKey)) {\n console.error(\n `Invalid query key format: \"${queryKey}\". Only alphanumeric characters, underscores, and hyphens are allowed.`,\n );\n return null;\n }\n\n const queryFilePath = path.join(\n process.cwd(),\n \"config/queries\",\n `${queryKey}.sql`,\n );\n\n // Security: Validate resolved path is within queries directory\n const resolvedPath = path.resolve(queryFilePath);\n const queriesDir = path.resolve(process.cwd(), \"config/queries\");\n\n if (!resolvedPath.startsWith(queriesDir)) {\n console.error(`Invalid query path: path traversal detected`);\n return null;\n }\n\n // Check if we're in dev mode and should use WebSocket\n const isDevMode = req?.query?.dev !== undefined;\n\n if (isDevMode && devFileReader && req) {\n try {\n // Read from local filesystem via WebSocket tunnel\n const relativePath = path.relative(process.cwd(), resolvedPath);\n return await devFileReader.readFile(relativePath, req);\n } catch (error) {\n console.error(\n `Failed to read query \"${queryKey}\" from dev tunnel: ${(error as Error).message}`,\n );\n return null;\n }\n }\n\n // Production mode: read from server filesystem\n try {\n const query = await fs.readFile(resolvedPath, \"utf8\");\n return query;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n console.error(`Query \"${queryKey}\" not found at path: ${resolvedPath}`);\n return null;\n }\n console.error(\n `Failed to read query \"${queryKey}\" from server filesystem: ${(error as Error).message}`,\n );\n return null;\n }\n }\n}\n\nexport type { DevFileReader, RequestLike };\n"],"mappings":";;;;AAYA,IAAa,aAAb,MAAwB;;;;;;;;;;CAUtB,MAAM,YACJ,UACA,KACA,eACwB;AAExB,MAAI,CAAC,YAAY,CAAC,mBAAmB,KAAK,SAAS,EAAE;AACnD,WAAQ,MACN,8BAA8B,SAAS,wEACxC;AACD,UAAO;;EAGT,MAAM,gBAAgB,KAAK,KACzB,QAAQ,KAAK,EACb,kBACA,GAAG,SAAS,MACb;EAGD,MAAM,eAAe,KAAK,QAAQ,cAAc;EAChD,MAAM,aAAa,KAAK,QAAQ,QAAQ,KAAK,EAAE,iBAAiB;AAEhE,MAAI,CAAC,aAAa,WAAW,WAAW,EAAE;AACxC,WAAQ,MAAM,8CAA8C;AAC5D,UAAO;;AAMT,MAFkB,KAAK,OAAO,QAAQ,UAErB,iBAAiB,IAChC,KAAI;GAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,KAAK,EAAE,aAAa;AAC/D,UAAO,MAAM,cAAc,SAAS,cAAc,IAAI;WAC/C,OAAO;AACd,WAAQ,MACN,yBAAyB,SAAS,qBAAsB,MAAgB,UACzE;AACD,UAAO;;AAKX,MAAI;AAEF,UADc,MAAM,GAAG,SAAS,cAAc,OAAO;WAE9C,OAAO;AACd,OAAK,MAAgC,SAAS,UAAU;AACtD,YAAQ,MAAM,UAAU,SAAS,uBAAuB,eAAe;AACvE,WAAO;;AAET,WAAQ,MACN,yBAAyB,SAAS,4BAA6B,MAAgB,UAChF;AACD,UAAO"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":["import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"app\");\n\ninterface RequestLike {\n query?: Record<string, any>;\n headers: Record<string, string | string[] | undefined>;\n}\n\ninterface DevFileReader {\n readFile(filePath: string, req: RequestLike): Promise<string>;\n}\n\nexport class AppManager {\n /**\n * Retrieves a query file by key from the queries directory\n * In dev mode with a request context, reads from local filesystem via WebSocket\n * @param queryKey - The query file name (without extension)\n * @param req - Optional request object to detect dev mode\n * @param devFileReader - Optional DevFileReader instance to read files from local filesystem\n * @returns The query content as a string\n * @throws Error if query key is invalid or file not found\n */\n async getAppQuery(\n queryKey: string,\n req?: RequestLike,\n devFileReader?: DevFileReader,\n ): Promise<string | null> {\n // Security: Sanitize query key to prevent path traversal\n if (!queryKey || !/^[a-zA-Z0-9_-]+$/.test(queryKey)) {\n logger.error(\n \"Invalid query key format: %s. Only alphanumeric characters, underscores, and hyphens are allowed.\",\n queryKey,\n );\n return null;\n }\n\n const queryFilePath = path.join(\n process.cwd(),\n \"config/queries\",\n `${queryKey}.sql`,\n );\n\n // Security: Validate resolved path is within queries directory\n const resolvedPath = path.resolve(queryFilePath);\n const queriesDir = path.resolve(process.cwd(), \"config/queries\");\n\n if (!resolvedPath.startsWith(queriesDir)) {\n logger.error(\"Invalid query path: path traversal detected\");\n return null;\n }\n\n // Check if we're in dev mode and should use WebSocket\n const isDevMode = req?.query?.dev !== undefined;\n\n if (isDevMode && devFileReader && req) {\n try {\n // Read from local filesystem via WebSocket tunnel\n const relativePath = path.relative(process.cwd(), resolvedPath);\n return await devFileReader.readFile(relativePath, req);\n } catch (error) {\n logger.error(\n \"Failed to read query %s from dev tunnel: %s\",\n queryKey,\n (error as Error).message,\n );\n return null;\n }\n }\n\n // Production mode: read from server filesystem\n try {\n const query = await fs.readFile(resolvedPath, \"utf8\");\n return query;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\") {\n logger.debug(\"Query %s not found at path: %s\", queryKey, resolvedPath);\n return null;\n }\n logger.error(\n \"Failed to read query %s from server filesystem: %s\",\n queryKey,\n (error as Error).message,\n );\n return null;\n }\n }\n}\n\nexport type { DevFileReader, RequestLike };\n"],"mappings":";;;;;AAIA,MAAM,SAAS,aAAa,MAAM;AAWlC,IAAa,aAAb,MAAwB;;;;;;;;;;CAUtB,MAAM,YACJ,UACA,KACA,eACwB;AAExB,MAAI,CAAC,YAAY,CAAC,mBAAmB,KAAK,SAAS,EAAE;AACnD,UAAO,MACL,qGACA,SACD;AACD,UAAO;;EAGT,MAAM,gBAAgB,KAAK,KACzB,QAAQ,KAAK,EACb,kBACA,GAAG,SAAS,MACb;EAGD,MAAM,eAAe,KAAK,QAAQ,cAAc;EAChD,MAAM,aAAa,KAAK,QAAQ,QAAQ,KAAK,EAAE,iBAAiB;AAEhE,MAAI,CAAC,aAAa,WAAW,WAAW,EAAE;AACxC,UAAO,MAAM,8CAA8C;AAC3D,UAAO;;AAMT,MAFkB,KAAK,OAAO,QAAQ,UAErB,iBAAiB,IAChC,KAAI;GAEF,MAAM,eAAe,KAAK,SAAS,QAAQ,KAAK,EAAE,aAAa;AAC/D,UAAO,MAAM,cAAc,SAAS,cAAc,IAAI;WAC/C,OAAO;AACd,UAAO,MACL,+CACA,UACC,MAAgB,QAClB;AACD,UAAO;;AAKX,MAAI;AAEF,UADc,MAAM,GAAG,SAAS,cAAc,OAAO;WAE9C,OAAO;AACd,OAAK,MAAgC,SAAS,UAAU;AACtD,WAAO,MAAM,kCAAkC,UAAU,aAAa;AACtE,WAAO;;AAET,UAAO,MACL,sDACA,UACC,MAAgB,QAClB;AACD,UAAO"}
@@ -1,6 +1,6 @@
1
1
  //#region package.json
2
2
  var name = "@databricks/appkit";
3
- var version = "0.1.5";
3
+ var version = "0.2.0";
4
4
 
5
5
  //#endregion
6
6
  export { name, version };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":[],"mappings":";;;;;;AAwBA;;;;;;;;;;;;AAwP6B,cAxPhB,YAAA,CAwPgB;0BAkDlB,uBAAA;mBAEN,IAAA;iBAawB,QAAA;iBAMZ,WAAA;UAUS,OAAA;UA0BT,MAAA;UAQW,gBAAA;EAAO,QAAA,iBAAA;;;;;;;;;;;4BA3TP;;;;;;;;kCAkBX,QAAQ,eACpB,QAAQ;;;;;;;;;;;;;;;;;;;;;;+DAuFC,QAAQ;;MAGjB,QAAQ;;;;;;uBA2FgB,QAAQ;;;;;;;;;;6BAkD1B;;MAEN;;;;;;uBAawB;;WAMZ;;;;;;oBAUS;;;;;;;;;WA0BT;;;;;sBAQW"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":[],"mappings":";;;;;;AA4BA;;;;;;;;;;;;AA+Q6B,cA/QhB,YAAA,CA+QgB;0BAkDlB,uBAAA;mBAEN,IAAA;iBAawB,QAAA;iBAMZ,WAAA;UAUS,OAAA;UA0BT,MAAA;UAQW,gBAAA;EAAO,QAAA,iBAAA;;;;;;;;;;;4BAnVP;;;;;;;;kCAmBX,QAAQ,eACpB,QAAQ;;;;;;;;;;;;;;;;;;;;;;+DAuFC,QAAQ;;MAGjB,QAAQ;;;;;;uBAkHgB,QAAQ;;;;;;;;;;6BAkD1B;;MAEN;;;;;;uBAawB;;WAMZ;;;;;;oBAUS;;;;;;;;;WA0BT;;;;;sBAQW"}