@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.
- package/AGENTS.md +52 -0
- package/CLAUDE.md +52 -0
- package/NOTICE.md +2 -0
- package/README.md +21 -15
- package/bin/appkit-lint.js +129 -0
- package/dist/analytics/analytics.d.ts.map +1 -1
- package/dist/analytics/analytics.js +16 -3
- package/dist/analytics/analytics.js.map +1 -1
- package/dist/analytics/query.js +8 -2
- package/dist/analytics/query.js.map +1 -1
- package/dist/app/index.d.ts.map +1 -1
- package/dist/app/index.js +7 -5
- package/dist/app/index.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +24 -3
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/storage/persistent.js +12 -6
- package/dist/cache/storage/persistent.js.map +1 -1
- package/dist/connectors/lakebase/client.js +25 -14
- package/dist/connectors/lakebase/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +68 -28
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/context/service-context.js +13 -8
- package/dist/context/service-context.js.map +1 -1
- package/dist/errors/authentication.d.ts +38 -0
- package/dist/errors/authentication.d.ts.map +1 -0
- package/dist/errors/authentication.js +48 -0
- package/dist/errors/authentication.js.map +1 -0
- package/dist/errors/base.d.ts +58 -0
- package/dist/errors/base.d.ts.map +1 -0
- package/dist/errors/base.js +70 -0
- package/dist/errors/base.js.map +1 -0
- package/dist/errors/configuration.d.ts +38 -0
- package/dist/errors/configuration.d.ts.map +1 -0
- package/dist/errors/configuration.js +45 -0
- package/dist/errors/configuration.js.map +1 -0
- package/dist/errors/connection.d.ts +42 -0
- package/dist/errors/connection.d.ts.map +1 -0
- package/dist/errors/connection.js +54 -0
- package/dist/errors/connection.js.map +1 -0
- package/dist/errors/execution.d.ts +42 -0
- package/dist/errors/execution.d.ts.map +1 -0
- package/dist/errors/execution.js +51 -0
- package/dist/errors/execution.js.map +1 -0
- package/dist/errors/index.js +28 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/errors/initialization.d.ts +34 -0
- package/dist/errors/initialization.d.ts.map +1 -0
- package/dist/errors/initialization.js +42 -0
- package/dist/errors/initialization.js.map +1 -0
- package/dist/errors/server.d.ts +38 -0
- package/dist/errors/server.d.ts.map +1 -0
- package/dist/errors/server.js +45 -0
- package/dist/errors/server.js.map +1 -0
- package/dist/errors/tunnel.d.ts +38 -0
- package/dist/errors/tunnel.d.ts.map +1 -0
- package/dist/errors/tunnel.js +51 -0
- package/dist/errors/tunnel.js.map +1 -0
- package/dist/errors/validation.d.ts +36 -0
- package/dist/errors/validation.d.ts.map +1 -0
- package/dist/errors/validation.js +45 -0
- package/dist/errors/validation.js.map +1 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +179 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/sampling.js +56 -0
- package/dist/logging/sampling.js.map +1 -0
- package/dist/logging/wide-event-emitter.js +108 -0
- package/dist/logging/wide-event-emitter.js.map +1 -0
- package/dist/logging/wide-event.js +167 -0
- package/dist/logging/wide-event.js.map +1 -0
- package/dist/plugin/dev-reader.d.ts.map +1 -1
- package/dist/plugin/dev-reader.js +8 -3
- package/dist/plugin/dev-reader.js.map +1 -1
- package/dist/plugin/interceptors/cache.js.map +1 -1
- package/dist/plugin/interceptors/retry.js +10 -2
- package/dist/plugin/interceptors/retry.js.map +1 -1
- package/dist/plugin/interceptors/telemetry.js +24 -9
- package/dist/plugin/interceptors/telemetry.js.map +1 -1
- package/dist/plugin/interceptors/timeout.js +4 -0
- package/dist/plugin/interceptors/timeout.js.map +1 -1
- package/dist/plugin/plugin.d.ts +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +9 -4
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +22 -17
- package/dist/server/index.js.map +1 -1
- package/dist/server/remote-tunnel/remote-tunnel-controller.js +4 -2
- package/dist/server/remote-tunnel/remote-tunnel-controller.js.map +1 -1
- package/dist/server/remote-tunnel/remote-tunnel-manager.js +10 -8
- package/dist/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/server/vite-dev-server.js +8 -3
- package/dist/server/vite-dev-server.js.map +1 -1
- package/dist/stream/arrow-stream-processor.js +13 -6
- package/dist/stream/arrow-stream-processor.js.map +1 -1
- package/dist/stream/buffers.js +5 -1
- package/dist/stream/buffers.js.map +1 -1
- package/dist/stream/stream-manager.d.ts.map +1 -1
- package/dist/stream/stream-manager.js +47 -36
- package/dist/stream/stream-manager.js.map +1 -1
- package/dist/stream/types.js.map +1 -1
- package/dist/telemetry/index.d.ts +2 -2
- package/dist/telemetry/index.js +2 -2
- package/dist/telemetry/instrumentations.js +14 -10
- package/dist/telemetry/instrumentations.js.map +1 -1
- package/dist/telemetry/telemetry-manager.js +8 -6
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/telemetry/trace-sampler.js +33 -0
- package/dist/telemetry/trace-sampler.js.map +1 -0
- package/dist/type-generator/index.js +4 -2
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/query-registry.js +4 -2
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +5 -3
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/utils/env-validator.js +5 -1
- package/dist/utils/env-validator.js.map +1 -1
- package/dist/utils/path-exclusions.js +66 -0
- package/dist/utils/path-exclusions.js.map +1 -0
- package/llms.txt +52 -0
- 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
|
-
|
|
4
|
-
|
|
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
|
-
##
|
|
15
|
+
## Introduction
|
|
15
16
|
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
+
## Getting started
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
Follow the [Getting Started](https://databricks.github.io/appkit/docs/) guide to get started with AppKit.
|
|
23
30
|
|
|
24
|
-
|
|
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
|
-
|
|
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":";;;;;;;;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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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"}
|
package/dist/analytics/query.js
CHANGED
|
@@ -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))
|
|
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
|
|
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
|
|
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"}
|
package/dist/app/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/app/index.ts"],"sourcesContent":[],"mappings":";
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
+
logger.debug("Query %s not found at path: %s", queryKey, resolvedPath);
|
|
39
41
|
return null;
|
|
40
42
|
}
|
|
41
|
-
|
|
43
|
+
logger.error("Failed to read query %s from server filesystem: %s", queryKey, error.message);
|
|
42
44
|
return null;
|
|
43
45
|
}
|
|
44
46
|
}
|
package/dist/app/index.js.map
CHANGED
|
@@ -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
|
|
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"}
|
package/dist/appkit/package.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/cache/index.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
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"}
|