@databricks/appkit 0.70.0 → 0.72.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/CLAUDE.md +46 -0
- package/NOTICE.md +2 -2
- package/dist/appkit/package.js +1 -1
- package/dist/beta.d.ts +13 -1
- package/dist/beta.js +13 -1
- package/dist/cli/commands/agent/eval.js +112 -0
- package/dist/cli/commands/agent/eval.js.map +1 -0
- package/dist/cli/commands/agent/index.js +18 -0
- package/dist/cli/commands/agent/index.js.map +1 -0
- package/dist/cli/index.js +2 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/connectors/index.js +2 -0
- package/dist/connectors/mlflow/auth.d.ts +18 -0
- package/dist/connectors/mlflow/auth.d.ts.map +1 -0
- package/dist/connectors/mlflow/auth.js +50 -0
- package/dist/connectors/mlflow/auth.js.map +1 -0
- package/dist/connectors/mlflow/client.d.ts +51 -0
- package/dist/connectors/mlflow/client.d.ts.map +1 -0
- package/dist/connectors/mlflow/client.js +93 -0
- package/dist/connectors/mlflow/client.js.map +1 -0
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js +1 -0
- package/dist/core/appkit.js.map +1 -1
- package/dist/evals/define-eval.d.ts +26 -0
- package/dist/evals/define-eval.d.ts.map +1 -0
- package/dist/evals/define-eval.js +28 -0
- package/dist/evals/define-eval.js.map +1 -0
- package/dist/evals/discover.d.ts +20 -0
- package/dist/evals/discover.d.ts.map +1 -0
- package/dist/evals/discover.js +49 -0
- package/dist/evals/discover.js.map +1 -0
- package/dist/evals/http-driver.d.ts +33 -0
- package/dist/evals/http-driver.d.ts.map +1 -0
- package/dist/evals/http-driver.js +118 -0
- package/dist/evals/http-driver.js.map +1 -0
- package/dist/evals/index.js +13 -0
- package/dist/evals/judge.d.ts +26 -0
- package/dist/evals/judge.d.ts.map +1 -0
- package/dist/evals/judge.js +77 -0
- package/dist/evals/judge.js.map +1 -0
- package/dist/evals/matchers.d.ts +12 -0
- package/dist/evals/matchers.d.ts.map +1 -0
- package/dist/evals/matchers.js +26 -0
- package/dist/evals/matchers.js.map +1 -0
- package/dist/evals/mlflow-report.d.ts +36 -0
- package/dist/evals/mlflow-report.d.ts.map +1 -0
- package/dist/evals/mlflow-report.js +161 -0
- package/dist/evals/mlflow-report.js.map +1 -0
- package/dist/evals/mlflow-run.d.ts +11 -0
- package/dist/evals/mlflow-run.d.ts.map +1 -0
- package/dist/evals/mlflow-run.js +101 -0
- package/dist/evals/mlflow-run.js.map +1 -0
- package/dist/evals/pool.js +24 -0
- package/dist/evals/pool.js.map +1 -0
- package/dist/evals/report.d.ts +25 -0
- package/dist/evals/report.d.ts.map +1 -0
- package/dist/evals/report.js +57 -0
- package/dist/evals/report.js.map +1 -0
- package/dist/evals/run-eval.d.ts +20 -0
- package/dist/evals/run-eval.d.ts.map +1 -0
- package/dist/evals/run-eval.js +144 -0
- package/dist/evals/run-eval.js.map +1 -0
- package/dist/evals/run-evals.d.ts +85 -0
- package/dist/evals/run-evals.d.ts.map +1 -0
- package/dist/evals/run-evals.js +178 -0
- package/dist/evals/run-evals.js.map +1 -0
- package/dist/evals/types.d.ts +127 -0
- package/dist/evals/types.d.ts.map +1 -0
- package/dist/plugins/agents/agents.d.ts.map +1 -1
- package/dist/plugins/agents/agents.js +5 -2
- package/dist/plugins/agents/agents.js.map +1 -1
- package/dist/plugins/agents/mlflow.js +267 -11
- package/dist/plugins/agents/mlflow.js.map +1 -1
- package/dist/plugins/server/index.js +2 -2
- package/dist/plugins/server/index.js.map +1 -1
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js +3 -3
- package/dist/plugins/server/remote-tunnel/remote-tunnel-manager.js.map +1 -1
- package/dist/plugins/server/static-server.js +3 -3
- package/dist/plugins/server/static-server.js.map +1 -1
- package/dist/plugins/server/utils.js +3 -3
- package/dist/plugins/server/utils.js.map +1 -1
- package/dist/plugins/server/vite-dev-server.js +4 -4
- package/dist/plugins/server/vite-dev-server.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +33 -33
- package/dist/telemetry/index.js +2 -2
- package/dist/telemetry/telemetry-manager.d.ts +2 -1
- package/dist/telemetry/telemetry-manager.js +114 -26
- package/dist/telemetry/telemetry-manager.js.map +1 -1
- package/dist/type-generator/database/generate.js +3 -3
- package/dist/type-generator/database/generate.js.map +1 -1
- package/dist/type-generator/errors.js +45 -11
- package/dist/type-generator/errors.js.map +1 -1
- package/dist/type-generator/migration.js +2 -2
- package/dist/type-generator/migration.js.map +1 -1
- package/dist/type-generator/mv-registry/sync.js +3 -3
- package/dist/type-generator/mv-registry/sync.js.map +1 -1
- package/dist/type-generator/mv-registry/types.d.ts +5 -2
- package/dist/type-generator/mv-registry/types.d.ts.map +1 -1
- package/dist/type-generator/query-registry.js +3 -3
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/server-file-extractor.js +3 -3
- package/dist/type-generator/serving/server-file-extractor.js.map +1 -1
- package/docs/api/appkit/Class.MlflowClient.md +103 -0
- package/docs/api/appkit/Function.buildAssessments.md +16 -0
- package/docs/api/appkit/Function.configureJudge.md +18 -0
- package/docs/api/appkit/Function.createHttpDriver.md +18 -0
- package/docs/api/appkit/Function.defineEval.md +35 -0
- package/docs/api/appkit/Function.discoverEvalFiles.md +18 -0
- package/docs/api/appkit/Function.equals.md +18 -0
- package/docs/api/appkit/Function.evalGlyph.md +18 -0
- package/docs/api/appkit/Function.formatEvalDetail.md +18 -0
- package/docs/api/appkit/Function.formatEvalHeadline.md +18 -0
- package/docs/api/appkit/Function.formatEvalResults.md +18 -0
- package/docs/api/appkit/Function.formatSummaryLine.md +18 -0
- package/docs/api/appkit/Function.includes.md +18 -0
- package/docs/api/appkit/Function.isJudgeConfigured.md +10 -0
- package/docs/api/appkit/Function.matches.md +18 -0
- package/docs/api/appkit/Function.normalizeHost.md +18 -0
- package/docs/api/appkit/Function.reportToMlflow.md +23 -0
- package/docs/api/appkit/Function.resolveDatabricksAuth.md +16 -0
- package/docs/api/appkit/Function.runEval.md +19 -0
- package/docs/api/appkit/Function.runEvalsInDir.md +18 -0
- package/docs/api/appkit/Function.summarize.md +16 -0
- package/docs/api/appkit/Interface.AssertionHandle.md +54 -0
- package/docs/api/appkit/Interface.AssertionResult.md +48 -0
- package/docs/api/appkit/Interface.Assessment.md +83 -0
- package/docs/api/appkit/Interface.CustomJudgeSpec.md +30 -0
- package/docs/api/appkit/Interface.DatabricksAuth.md +21 -0
- package/docs/api/appkit/Interface.DiscoveredEval.md +36 -0
- package/docs/api/appkit/Interface.DriveResult.md +58 -0
- package/docs/api/appkit/Interface.EvalDefinition.md +46 -0
- package/docs/api/appkit/Interface.EvalDriver.md +22 -0
- package/docs/api/appkit/Interface.EvalResult.md +83 -0
- package/docs/api/appkit/Interface.EvalRunSummary.md +46 -0
- package/docs/api/appkit/Interface.EvalSummary.md +48 -0
- package/docs/api/appkit/Interface.HttpDriverOptions.md +67 -0
- package/docs/api/appkit/Interface.JudgeConfig.md +34 -0
- package/docs/api/appkit/Interface.JudgeScore.md +21 -0
- package/docs/api/appkit/Interface.MatchResult.md +34 -0
- package/docs/api/appkit/Interface.PostResult.md +30 -0
- package/docs/api/appkit/Interface.ReportOutcome.md +53 -0
- package/docs/api/appkit/Interface.ResolveDatabricksAuthOptions.md +34 -0
- package/docs/api/appkit/Interface.RunEvalOptions.md +34 -0
- package/docs/api/appkit/Interface.RunEvalsOptions.md +192 -0
- package/docs/api/appkit/Interface.TestContext.md +208 -0
- package/docs/api/appkit/TypeAlias.EvalProgress.md +26 -0
- package/docs/api/appkit/TypeAlias.Matcher.md +18 -0
- package/docs/api/appkit/TypeAlias.Severity.md +8 -0
- package/docs/api/appkit.md +63 -17
- package/llms.txt +46 -0
- package/package.json +5 -4
- package/sbom.cdx.json +1 -1
|
@@ -1,24 +1,49 @@
|
|
|
1
1
|
import { createLogger } from "../logging/logger.js";
|
|
2
2
|
import { TelemetryProvider } from "./telemetry-provider.js";
|
|
3
3
|
import { AppKitSampler } from "./trace-sampler.js";
|
|
4
|
+
import { metrics } from "@opentelemetry/api";
|
|
5
|
+
import { logs } from "@opentelemetry/api-logs";
|
|
4
6
|
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
5
7
|
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-proto";
|
|
6
8
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-proto";
|
|
7
9
|
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
8
10
|
import { registerInstrumentations } from "@opentelemetry/instrumentation";
|
|
9
11
|
import { detectResources, envDetector, hostDetector, processDetector, resourceFromAttributes } from "@opentelemetry/resources";
|
|
10
|
-
import { BatchLogRecordProcessor } from "@opentelemetry/sdk-logs";
|
|
11
|
-
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
12
|
-
import {
|
|
12
|
+
import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
|
|
13
|
+
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
14
|
+
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
15
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
13
16
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
14
17
|
|
|
15
18
|
//#region src/telemetry/telemetry-manager.ts
|
|
16
19
|
const logger = createLogger("telemetry");
|
|
20
|
+
/**
|
|
21
|
+
* Owns the app's OpenTelemetry providers, split into two phases so plugins can
|
|
22
|
+
* contribute trace span processors before the tracer provider is built.
|
|
23
|
+
*
|
|
24
|
+
* - `initialize()` runs at app bootstrap, before plugin setup. It registers the
|
|
25
|
+
* meter and logger providers eagerly, because OTel's metrics API has no lazy
|
|
26
|
+
* proxy: a counter/histogram bound against the NoOp meter (as every connector
|
|
27
|
+
* and the cache do in their constructors) stays NoOp for the process lifetime.
|
|
28
|
+
* It does NOT register a tracer provider.
|
|
29
|
+
* - `registerSpanProcessor()` is called by plugins during `setup()` to add a
|
|
30
|
+
* span processor (e.g. an MLflow exporter) to the not-yet-built tracer.
|
|
31
|
+
* - `start()` runs after all plugin `setup()` completes. It builds the single
|
|
32
|
+
* global tracer provider with the OTLP processor (if configured) plus every
|
|
33
|
+
* contributed processor. Deferring is safe for traces: OTel's ProxyTracer
|
|
34
|
+
* rebinds tracers obtained before registration, and no span is emitted during
|
|
35
|
+
* setup.
|
|
36
|
+
*/
|
|
17
37
|
var TelemetryManager = class TelemetryManager {
|
|
18
38
|
static DEFAULT_EXPORT_INTERVAL_MS = 1e4;
|
|
19
39
|
static DEFAULT_FALLBACK_APP_NAME = "databricks-app";
|
|
20
40
|
static instance;
|
|
21
|
-
|
|
41
|
+
resource;
|
|
42
|
+
meterProvider;
|
|
43
|
+
loggerProvider;
|
|
44
|
+
tracerProvider;
|
|
45
|
+
spanProcessors = [];
|
|
46
|
+
started = false;
|
|
22
47
|
shutdownPromise;
|
|
23
48
|
/**
|
|
24
49
|
* Create a scoped telemetry provider for a specific plugin.
|
|
@@ -38,29 +63,84 @@ var TelemetryManager = class TelemetryManager {
|
|
|
38
63
|
static initialize(config = {}) {
|
|
39
64
|
TelemetryManager.getInstance()._initialize(config);
|
|
40
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Contribute a span processor to the not-yet-built tracer provider. Called by
|
|
68
|
+
* plugins during `setup()`. No-op with a warning once `start()` has run, since
|
|
69
|
+
* a started provider's processors are immutable in OTel JS 2.x.
|
|
70
|
+
*/
|
|
71
|
+
static registerSpanProcessor(processor) {
|
|
72
|
+
TelemetryManager.getInstance()._registerSpanProcessor(processor);
|
|
73
|
+
}
|
|
74
|
+
_registerSpanProcessor(processor) {
|
|
75
|
+
if (this.started) {
|
|
76
|
+
logger.warn("registerSpanProcessor called after start(); processor ignored. Contribute span processors during plugin setup().");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
this.spanProcessors.push(processor);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Phase 1: register the meter and logger providers eagerly (before plugin
|
|
83
|
+
* setup), so metric instruments bound in connector/cache constructors attach
|
|
84
|
+
* to real meters. The tracer provider is deferred to `start()`.
|
|
85
|
+
*
|
|
86
|
+
* When no OTLP endpoint is configured, meter/logger registration is skipped;
|
|
87
|
+
* a contributed span processor can still bring up tracing in `start()`.
|
|
88
|
+
*/
|
|
41
89
|
_initialize(config) {
|
|
42
|
-
if (this.
|
|
90
|
+
if (this.resource) return;
|
|
91
|
+
this.resource = this.createResource(config);
|
|
43
92
|
if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return;
|
|
44
93
|
try {
|
|
45
|
-
this.
|
|
46
|
-
resource: this.
|
|
47
|
-
|
|
48
|
-
sampler: new AppKitSampler(),
|
|
49
|
-
traceExporter: new OTLPTraceExporter({ headers: config.headers }),
|
|
50
|
-
metricReaders: [new PeriodicExportingMetricReader({
|
|
94
|
+
this.meterProvider = new MeterProvider({
|
|
95
|
+
resource: this.resource,
|
|
96
|
+
readers: [new PeriodicExportingMetricReader({
|
|
51
97
|
exporter: new OTLPMetricExporter({ headers: config.headers }),
|
|
52
98
|
exportIntervalMillis: config.exportIntervalMs || TelemetryManager.DEFAULT_EXPORT_INTERVAL_MS
|
|
53
|
-
})]
|
|
54
|
-
|
|
55
|
-
|
|
99
|
+
})]
|
|
100
|
+
});
|
|
101
|
+
metrics.setGlobalMeterProvider(this.meterProvider);
|
|
102
|
+
this.loggerProvider = new LoggerProvider({
|
|
103
|
+
resource: this.resource,
|
|
104
|
+
processors: [new BatchLogRecordProcessor(new OTLPLogExporter({ headers: config.headers }))]
|
|
56
105
|
});
|
|
57
|
-
this.
|
|
58
|
-
|
|
106
|
+
logs.setGlobalLoggerProvider(this.loggerProvider);
|
|
107
|
+
this.spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({ headers: config.headers })));
|
|
108
|
+
this.registerInstrumentations(this.getDefaultInstrumentations());
|
|
109
|
+
logger.debug("Meter/logger providers initialized");
|
|
59
110
|
} catch (error) {
|
|
60
111
|
logger.error("Failed to initialize: %O", error);
|
|
61
112
|
}
|
|
62
113
|
}
|
|
63
114
|
/**
|
|
115
|
+
* Phase 2: build and register the global tracer provider. Called by core
|
|
116
|
+
* after every plugin's `setup()` completes, so all contributed span
|
|
117
|
+
* processors are known. No-op when nothing needs tracing (no OTLP endpoint
|
|
118
|
+
* and no contributed processor), preserving "no telemetry unless configured".
|
|
119
|
+
*
|
|
120
|
+
* `NodeTracerProvider.register()` installs the async-hooks context manager and
|
|
121
|
+
* W3C propagators — the same wiring `NodeSDK.start()` did — so span nesting
|
|
122
|
+
* across awaits is preserved.
|
|
123
|
+
*/
|
|
124
|
+
static start() {
|
|
125
|
+
TelemetryManager.getInstance()._start();
|
|
126
|
+
}
|
|
127
|
+
_start() {
|
|
128
|
+
if (this.started) return;
|
|
129
|
+
this.started = true;
|
|
130
|
+
if (this.spanProcessors.length === 0) return;
|
|
131
|
+
try {
|
|
132
|
+
this.tracerProvider = new NodeTracerProvider({
|
|
133
|
+
resource: this.resource,
|
|
134
|
+
sampler: new AppKitSampler(),
|
|
135
|
+
spanProcessors: this.spanProcessors
|
|
136
|
+
});
|
|
137
|
+
this.tracerProvider.register();
|
|
138
|
+
logger.debug("Tracer provider started with %d span processor(s)", this.spanProcessors.length);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
logger.error("Failed to start tracer provider: %O", error);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
64
144
|
* Register OpenTelemetry instrumentations.
|
|
65
145
|
* Can be called at any time, but recommended to call in plugin constructor.
|
|
66
146
|
* @param instrumentations - Array of OpenTelemetry instrumentations to register
|
|
@@ -91,23 +171,31 @@ var TelemetryManager = class TelemetryManager {
|
|
|
91
171
|
})];
|
|
92
172
|
}
|
|
93
173
|
/**
|
|
94
|
-
* Flush and shut down the
|
|
174
|
+
* Flush and shut down the tracer, meter, and logger providers.
|
|
95
175
|
*
|
|
96
|
-
* Idempotent: the
|
|
176
|
+
* Idempotent: the provider references are cleared synchronously and concurrent
|
|
97
177
|
* or repeated calls await the same in-flight flush. Awaited by the core
|
|
98
178
|
* lifecycle manager during graceful shutdown — that manager owns the
|
|
99
179
|
* process signal handlers, so telemetry no longer registers its own.
|
|
100
180
|
*/
|
|
101
181
|
async shutdown() {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
this.
|
|
182
|
+
const providers = [
|
|
183
|
+
this.tracerProvider,
|
|
184
|
+
this.meterProvider,
|
|
185
|
+
this.loggerProvider
|
|
186
|
+
].filter((p) => p !== void 0);
|
|
187
|
+
if (providers.length > 0) {
|
|
188
|
+
this.tracerProvider = void 0;
|
|
189
|
+
this.meterProvider = void 0;
|
|
190
|
+
this.loggerProvider = void 0;
|
|
105
191
|
this.shutdownPromise = (async () => {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
192
|
+
await Promise.all(providers.map(async (provider) => {
|
|
193
|
+
try {
|
|
194
|
+
await provider.shutdown();
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error("Error shutting down: %O", error);
|
|
197
|
+
}
|
|
198
|
+
}));
|
|
111
199
|
})();
|
|
112
200
|
}
|
|
113
201
|
return this.shutdownPromise;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"telemetry-manager.js","names":[],"sources":["../../src/telemetry/telemetry-manager.ts"],"sourcesContent":["import { getNodeAutoInstrumentations } from \"@opentelemetry/auto-instrumentations-node\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-proto\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-proto\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-proto\";\nimport {\n type Instrumentation,\n registerInstrumentations as otelRegisterInstrumentations,\n} from \"@opentelemetry/instrumentation\";\nimport {\n detectResources,\n envDetector,\n hostDetector,\n processDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport { BatchLogRecordProcessor } from \"@opentelemetry/sdk-logs\";\nimport { PeriodicExportingMetricReader } from \"@opentelemetry/sdk-metrics\";\nimport { NodeSDK } from \"@opentelemetry/sdk-node\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport type { TelemetryOptions } from \"shared\";\n\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryProvider } from \"./telemetry-provider\";\nimport { AppKitSampler } from \"./trace-sampler\";\nimport type { TelemetryConfig } from \"./types\";\n\nconst logger = createLogger(\"telemetry\");\n\nexport class TelemetryManager {\n private static readonly DEFAULT_EXPORT_INTERVAL_MS = 10000;\n private static readonly DEFAULT_FALLBACK_APP_NAME = \"databricks-app\";\n\n private static instance?: TelemetryManager;\n private sdk?: NodeSDK;\n private shutdownPromise?: Promise<void>;\n\n /**\n * Create a scoped telemetry provider for a specific plugin.\n * The plugin's name will be used as the default tracer/meter name.\n * @param pluginName - The name of the plugin to create scoped telemetry for\n * @param telemetryConfig - The telemetry configuration for the plugin\n * @returns A scoped telemetry instance for the plugin\n */\n static getProvider(\n pluginName: string,\n telemetryConfig?: TelemetryOptions,\n ): TelemetryProvider {\n const globalManager = TelemetryManager.getInstance();\n return new TelemetryProvider(pluginName, globalManager, telemetryConfig);\n }\n\n private constructor() {}\n\n static getInstance(): TelemetryManager {\n if (!TelemetryManager.instance) {\n TelemetryManager.instance = new TelemetryManager();\n }\n return TelemetryManager.instance;\n }\n\n static initialize(config: Partial<TelemetryConfig> = {}): void {\n const instance = TelemetryManager.getInstance();\n instance._initialize(config);\n }\n\n private _initialize(config: Partial<TelemetryConfig>): void {\n if (this.sdk) return;\n\n if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {\n return;\n }\n\n try {\n this.sdk = new NodeSDK({\n resource: this.createResource(config),\n autoDetectResources: false,\n sampler: new AppKitSampler(),\n traceExporter: new OTLPTraceExporter({ headers: config.headers }),\n metricReaders: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({ headers: config.headers }),\n exportIntervalMillis:\n config.exportIntervalMs ||\n TelemetryManager.DEFAULT_EXPORT_INTERVAL_MS,\n }),\n ],\n logRecordProcessors: [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({ headers: config.headers }),\n ),\n ],\n instrumentations: this.getDefaultInstrumentations(),\n });\n\n this.sdk.start();\n logger.debug(\"Initialized successfully\");\n } catch (error) {\n logger.error(\"Failed to initialize: %O\", error);\n }\n }\n\n /**\n * Register OpenTelemetry instrumentations.\n * Can be called at any time, but recommended to call in plugin constructor.\n * @param instrumentations - Array of OpenTelemetry instrumentations to register\n */\n registerInstrumentations(instrumentations: Instrumentation[]): void {\n otelRegisterInstrumentations({\n // global providers set by NodeSDK.start()\n instrumentations,\n });\n }\n\n private createResource(config: Partial<TelemetryConfig>): Resource {\n const serviceName =\n config.serviceName ||\n process.env.OTEL_SERVICE_NAME ||\n process.env.DATABRICKS_APP_NAME ||\n TelemetryManager.DEFAULT_FALLBACK_APP_NAME;\n const initialResource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? undefined,\n });\n const detectedResource = detectResources({\n detectors: [envDetector, hostDetector, processDetector],\n });\n return initialResource.merge(detectedResource);\n }\n\n private getDefaultInstrumentations(): Instrumentation[] {\n return [\n ...getNodeAutoInstrumentations({\n //\n // enabled as a part of the server plugin\n //\n \"@opentelemetry/instrumentation-http\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-express\": {\n enabled: false,\n },\n //\n // reduce noise\n //\n \"@opentelemetry/instrumentation-fs\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-dns\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-net\": {\n enabled: false,\n },\n }),\n ];\n }\n\n /**\n * Flush and shut down the OpenTelemetry SDK.\n *\n * Idempotent: the SDK reference is cleared synchronously and concurrent\n * or repeated calls await the same in-flight flush. Awaited by the core\n * lifecycle manager during graceful shutdown — that manager owns the\n * process signal handlers, so telemetry no longer registers its own.\n */\n async shutdown(): Promise<void> {\n if (this.sdk) {\n const sdk = this.sdk;\n this.sdk = undefined;\n this.shutdownPromise = (async () => {\n try {\n await sdk.shutdown();\n } catch (error) {\n logger.error(\"Error shutting down: %O\", error);\n }\n })();\n }\n\n return this.shutdownPromise;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;AA8BA,MAAM,SAAS,aAAa,YAAY;AAExC,IAAa,mBAAb,MAAa,iBAAiB;CAC5B,OAAwB,6BAA6B;CACrD,OAAwB,4BAA4B;CAEpD,OAAe;CACf,AAAQ;CACR,AAAQ;;;;;;;;CASR,OAAO,YACL,YACA,iBACmB;AAEnB,SAAO,IAAI,kBAAkB,YADP,iBAAiB,aAAa,EACI,gBAAgB;;CAG1E,AAAQ,cAAc;CAEtB,OAAO,cAAgC;AACrC,MAAI,CAAC,iBAAiB,SACpB,kBAAiB,WAAW,IAAI,kBAAkB;AAEpD,SAAO,iBAAiB;;CAG1B,OAAO,WAAW,SAAmC,EAAE,EAAQ;AAE7D,EADiB,iBAAiB,aAAa,CACtC,YAAY,OAAO;;CAG9B,AAAQ,YAAY,QAAwC;AAC1D,MAAI,KAAK,IAAK;AAEd,MAAI,CAAC,QAAQ,IAAI,4BACf;AAGF,MAAI;AACF,QAAK,MAAM,IAAI,QAAQ;IACrB,UAAU,KAAK,eAAe,OAAO;IACrC,qBAAqB;IACrB,SAAS,IAAI,eAAe;IAC5B,eAAe,IAAI,kBAAkB,EAAE,SAAS,OAAO,SAAS,CAAC;IACjE,eAAe,CACb,IAAI,8BAA8B;KAChC,UAAU,IAAI,mBAAmB,EAAE,SAAS,OAAO,SAAS,CAAC;KAC7D,sBACE,OAAO,oBACP,iBAAiB;KACpB,CAAC,CACH;IACD,qBAAqB,CACnB,IAAI,wBACF,IAAI,gBAAgB,EAAE,SAAS,OAAO,SAAS,CAAC,CACjD,CACF;IACD,kBAAkB,KAAK,4BAA4B;IACpD,CAAC;AAEF,QAAK,IAAI,OAAO;AAChB,UAAO,MAAM,2BAA2B;WACjC,OAAO;AACd,UAAO,MAAM,4BAA4B,MAAM;;;;;;;;CASnD,yBAAyB,kBAA2C;AAClE,2BAA6B,EAE3B,kBACD,CAAC;;CAGJ,AAAQ,eAAe,QAA4C;EACjE,MAAM,cACJ,OAAO,eACP,QAAQ,IAAI,qBACZ,QAAQ,IAAI,uBACZ,iBAAiB;EACnB,MAAM,kBAAkB,uBAAuB;IAC5C,oBAAoB;IACpB,uBAAuB,OAAO,kBAAkB;GAClD,CAAC;EACF,MAAM,mBAAmB,gBAAgB,EACvC,WAAW;GAAC;GAAa;GAAc;GAAgB,EACxD,CAAC;AACF,SAAO,gBAAgB,MAAM,iBAAiB;;CAGhD,AAAQ,6BAAgD;AACtD,SAAO,CACL,GAAG,4BAA4B;GAI7B,uCAAuC,EACrC,SAAS,OACV;GACD,0CAA0C,EACxC,SAAS,OACV;GAID,qCAAqC,EACnC,SAAS,OACV;GACD,sCAAsC,EACpC,SAAS,OACV;GACD,sCAAsC,EACpC,SAAS,OACV;GACF,CAAC,CACH;;;;;;;;;;CAWH,MAAM,WAA0B;AAC9B,MAAI,KAAK,KAAK;GACZ,MAAM,MAAM,KAAK;AACjB,QAAK,MAAM;AACX,QAAK,mBAAmB,YAAY;AAClC,QAAI;AACF,WAAM,IAAI,UAAU;aACb,OAAO;AACd,YAAO,MAAM,2BAA2B,MAAM;;OAE9C;;AAGN,SAAO,KAAK"}
|
|
1
|
+
{"version":3,"file":"telemetry-manager.js","names":[],"sources":["../../src/telemetry/telemetry-manager.ts"],"sourcesContent":["import { metrics } from \"@opentelemetry/api\";\nimport { logs } from \"@opentelemetry/api-logs\";\nimport { getNodeAutoInstrumentations } from \"@opentelemetry/auto-instrumentations-node\";\nimport { OTLPLogExporter } from \"@opentelemetry/exporter-logs-otlp-proto\";\nimport { OTLPMetricExporter } from \"@opentelemetry/exporter-metrics-otlp-proto\";\nimport { OTLPTraceExporter } from \"@opentelemetry/exporter-trace-otlp-proto\";\nimport {\n type Instrumentation,\n registerInstrumentations as otelRegisterInstrumentations,\n} from \"@opentelemetry/instrumentation\";\nimport {\n detectResources,\n envDetector,\n hostDetector,\n processDetector,\n type Resource,\n resourceFromAttributes,\n} from \"@opentelemetry/resources\";\nimport {\n BatchLogRecordProcessor,\n LoggerProvider,\n} from \"@opentelemetry/sdk-logs\";\nimport {\n MeterProvider,\n PeriodicExportingMetricReader,\n} from \"@opentelemetry/sdk-metrics\";\nimport {\n BatchSpanProcessor,\n type SpanProcessor,\n} from \"@opentelemetry/sdk-trace-base\";\nimport { NodeTracerProvider } from \"@opentelemetry/sdk-trace-node\";\nimport {\n ATTR_SERVICE_NAME,\n ATTR_SERVICE_VERSION,\n} from \"@opentelemetry/semantic-conventions\";\nimport type { TelemetryOptions } from \"shared\";\n\nimport { createLogger } from \"../logging/logger\";\nimport { TelemetryProvider } from \"./telemetry-provider\";\nimport { AppKitSampler } from \"./trace-sampler\";\nimport type { TelemetryConfig } from \"./types\";\n\nconst logger = createLogger(\"telemetry\");\n\n/**\n * Owns the app's OpenTelemetry providers, split into two phases so plugins can\n * contribute trace span processors before the tracer provider is built.\n *\n * - `initialize()` runs at app bootstrap, before plugin setup. It registers the\n * meter and logger providers eagerly, because OTel's metrics API has no lazy\n * proxy: a counter/histogram bound against the NoOp meter (as every connector\n * and the cache do in their constructors) stays NoOp for the process lifetime.\n * It does NOT register a tracer provider.\n * - `registerSpanProcessor()` is called by plugins during `setup()` to add a\n * span processor (e.g. an MLflow exporter) to the not-yet-built tracer.\n * - `start()` runs after all plugin `setup()` completes. It builds the single\n * global tracer provider with the OTLP processor (if configured) plus every\n * contributed processor. Deferring is safe for traces: OTel's ProxyTracer\n * rebinds tracers obtained before registration, and no span is emitted during\n * setup.\n */\nexport class TelemetryManager {\n private static readonly DEFAULT_EXPORT_INTERVAL_MS = 10000;\n private static readonly DEFAULT_FALLBACK_APP_NAME = \"databricks-app\";\n\n private static instance?: TelemetryManager;\n private resource?: Resource;\n private meterProvider?: MeterProvider;\n private loggerProvider?: LoggerProvider;\n private tracerProvider?: NodeTracerProvider;\n private readonly spanProcessors: SpanProcessor[] = [];\n private started = false;\n private shutdownPromise?: Promise<void>;\n\n /**\n * Create a scoped telemetry provider for a specific plugin.\n * The plugin's name will be used as the default tracer/meter name.\n * @param pluginName - The name of the plugin to create scoped telemetry for\n * @param telemetryConfig - The telemetry configuration for the plugin\n * @returns A scoped telemetry instance for the plugin\n */\n static getProvider(\n pluginName: string,\n telemetryConfig?: TelemetryOptions,\n ): TelemetryProvider {\n const globalManager = TelemetryManager.getInstance();\n return new TelemetryProvider(pluginName, globalManager, telemetryConfig);\n }\n\n private constructor() {}\n\n static getInstance(): TelemetryManager {\n if (!TelemetryManager.instance) {\n TelemetryManager.instance = new TelemetryManager();\n }\n return TelemetryManager.instance;\n }\n\n static initialize(config: Partial<TelemetryConfig> = {}): void {\n const instance = TelemetryManager.getInstance();\n instance._initialize(config);\n }\n\n /**\n * Contribute a span processor to the not-yet-built tracer provider. Called by\n * plugins during `setup()`. No-op with a warning once `start()` has run, since\n * a started provider's processors are immutable in OTel JS 2.x.\n */\n static registerSpanProcessor(processor: SpanProcessor): void {\n TelemetryManager.getInstance()._registerSpanProcessor(processor);\n }\n\n private _registerSpanProcessor(processor: SpanProcessor): void {\n if (this.started) {\n logger.warn(\n \"registerSpanProcessor called after start(); processor ignored. \" +\n \"Contribute span processors during plugin setup().\",\n );\n return;\n }\n this.spanProcessors.push(processor);\n }\n\n /**\n * Phase 1: register the meter and logger providers eagerly (before plugin\n * setup), so metric instruments bound in connector/cache constructors attach\n * to real meters. The tracer provider is deferred to `start()`.\n *\n * When no OTLP endpoint is configured, meter/logger registration is skipped;\n * a contributed span processor can still bring up tracing in `start()`.\n */\n private _initialize(config: Partial<TelemetryConfig>): void {\n if (this.resource) return;\n this.resource = this.createResource(config);\n\n // OTLP exporters need an endpoint. Without one there is nothing to export\n // metrics/logs to, so skip those providers — but still capture the resource\n // and let `start()` bring up a tracer if a plugin contributed a processor.\n if (!process.env.OTEL_EXPORTER_OTLP_ENDPOINT) {\n return;\n }\n\n try {\n this.meterProvider = new MeterProvider({\n resource: this.resource,\n readers: [\n new PeriodicExportingMetricReader({\n exporter: new OTLPMetricExporter({ headers: config.headers }),\n exportIntervalMillis:\n config.exportIntervalMs ||\n TelemetryManager.DEFAULT_EXPORT_INTERVAL_MS,\n }),\n ],\n });\n metrics.setGlobalMeterProvider(this.meterProvider);\n\n this.loggerProvider = new LoggerProvider({\n resource: this.resource,\n processors: [\n new BatchLogRecordProcessor(\n new OTLPLogExporter({ headers: config.headers }),\n ),\n ],\n });\n logs.setGlobalLoggerProvider(this.loggerProvider);\n\n // The OTLP trace exporter is the first span processor; contributed\n // processors join it in `start()`.\n this.spanProcessors.push(\n new BatchSpanProcessor(\n new OTLPTraceExporter({ headers: config.headers }),\n ),\n );\n\n this.registerInstrumentations(this.getDefaultInstrumentations());\n logger.debug(\"Meter/logger providers initialized\");\n } catch (error) {\n logger.error(\"Failed to initialize: %O\", error);\n }\n }\n\n /**\n * Phase 2: build and register the global tracer provider. Called by core\n * after every plugin's `setup()` completes, so all contributed span\n * processors are known. No-op when nothing needs tracing (no OTLP endpoint\n * and no contributed processor), preserving \"no telemetry unless configured\".\n *\n * `NodeTracerProvider.register()` installs the async-hooks context manager and\n * W3C propagators — the same wiring `NodeSDK.start()` did — so span nesting\n * across awaits is preserved.\n */\n static start(): void {\n TelemetryManager.getInstance()._start();\n }\n\n private _start(): void {\n if (this.started) return;\n this.started = true;\n\n if (this.spanProcessors.length === 0) {\n return;\n }\n\n try {\n this.tracerProvider = new NodeTracerProvider({\n resource: this.resource,\n sampler: new AppKitSampler(),\n spanProcessors: this.spanProcessors,\n });\n this.tracerProvider.register();\n logger.debug(\n \"Tracer provider started with %d span processor(s)\",\n this.spanProcessors.length,\n );\n } catch (error) {\n logger.error(\"Failed to start tracer provider: %O\", error);\n }\n }\n\n /**\n * Register OpenTelemetry instrumentations.\n * Can be called at any time, but recommended to call in plugin constructor.\n * @param instrumentations - Array of OpenTelemetry instrumentations to register\n */\n registerInstrumentations(instrumentations: Instrumentation[]): void {\n otelRegisterInstrumentations({\n // Instrumentations bind to the global providers registered by start()\n // (tracer) and _initialize() (meter/logger).\n instrumentations,\n });\n }\n\n private createResource(config: Partial<TelemetryConfig>): Resource {\n const serviceName =\n config.serviceName ||\n process.env.OTEL_SERVICE_NAME ||\n process.env.DATABRICKS_APP_NAME ||\n TelemetryManager.DEFAULT_FALLBACK_APP_NAME;\n const initialResource = resourceFromAttributes({\n [ATTR_SERVICE_NAME]: serviceName,\n [ATTR_SERVICE_VERSION]: config.serviceVersion ?? undefined,\n });\n const detectedResource = detectResources({\n detectors: [envDetector, hostDetector, processDetector],\n });\n return initialResource.merge(detectedResource);\n }\n\n private getDefaultInstrumentations(): Instrumentation[] {\n return [\n ...getNodeAutoInstrumentations({\n //\n // enabled as a part of the server plugin\n //\n \"@opentelemetry/instrumentation-http\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-express\": {\n enabled: false,\n },\n //\n // reduce noise\n //\n \"@opentelemetry/instrumentation-fs\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-dns\": {\n enabled: false,\n },\n \"@opentelemetry/instrumentation-net\": {\n enabled: false,\n },\n }),\n ];\n }\n\n /**\n * Flush and shut down the tracer, meter, and logger providers.\n *\n * Idempotent: the provider references are cleared synchronously and concurrent\n * or repeated calls await the same in-flight flush. Awaited by the core\n * lifecycle manager during graceful shutdown — that manager owns the\n * process signal handlers, so telemetry no longer registers its own.\n */\n async shutdown(): Promise<void> {\n const providers = [\n this.tracerProvider,\n this.meterProvider,\n this.loggerProvider,\n ].filter((p): p is NonNullable<typeof p> => p !== undefined);\n\n if (providers.length > 0) {\n this.tracerProvider = undefined;\n this.meterProvider = undefined;\n this.loggerProvider = undefined;\n this.shutdownPromise = (async () => {\n await Promise.all(\n providers.map(async (provider) => {\n try {\n await provider.shutdown();\n } catch (error) {\n logger.error(\"Error shutting down: %O\", error);\n }\n }),\n );\n })();\n }\n\n return this.shutdownPromise;\n }\n}\n"],"mappings":"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"}
|
|
@@ -3,7 +3,7 @@ import { walkSchema } from "./walk-schema.js";
|
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
4
|
import fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import
|
|
6
|
+
import fs$1 from "node:fs";
|
|
7
7
|
|
|
8
8
|
//#region src/type-generator/database/generate.ts
|
|
9
9
|
/** Safe diagnostic raised when database declarations cannot be generated. */
|
|
@@ -63,7 +63,7 @@ ${walkSchema(schema).map((entry) => ` ${JSON.stringify(entry.name)}: {
|
|
|
63
63
|
}
|
|
64
64
|
/** Atomically replace generated output only when its content changes. */
|
|
65
65
|
async function writeIfChanged(outFile, content) {
|
|
66
|
-
if (
|
|
66
|
+
if (fs$1.existsSync(outFile) && await fs.readFile(outFile, "utf8") === content) return;
|
|
67
67
|
await fs.mkdir(path.dirname(outFile), { recursive: true });
|
|
68
68
|
const temporary = path.join(path.dirname(outFile), `.${path.basename(outFile)}.${randomUUID()}.tmp`);
|
|
69
69
|
try {
|
|
@@ -80,7 +80,7 @@ async function importSchema(schemaFile) {
|
|
|
80
80
|
}
|
|
81
81
|
/** Generate current registry types or neutralize them when loading fails. */
|
|
82
82
|
async function generateDatabaseTypes(options) {
|
|
83
|
-
if (!
|
|
83
|
+
if (!fs$1.existsSync(options.schemaFile)) {
|
|
84
84
|
await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.js","names":[],"sources":["../../../src/type-generator/database/generate.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fsSync from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\nimport { walkSchema } from \"./walk-schema\";\n\n/** Safe diagnostic raised when database declarations cannot be generated. */\nexport class DatabaseTypegenError extends Error {\n constructor(message = \"Database schema generation failed\") {\n super(message);\n this.name = \"DatabaseTypegenError\";\n }\n}\n\ninterface GenerateDatabaseTypesOptions {\n readonly schemaFile: string;\n readonly outFile: string;\n}\n\nexport const DATABASE_TYPES_FILE = \"database.d.ts\";\n\nfunction schemaLabel(schemaFile: string): string {\n return path.relative(process.cwd(), schemaFile) || schemaFile;\n}\n\nfunction importFailureReason(error: unknown): string {\n if (error instanceof SyntaxError) return \"could not be parsed\";\n let code: unknown;\n try {\n code =\n error && typeof error === \"object\"\n ? Reflect.get(error, \"code\")\n : undefined;\n } catch {\n code = undefined;\n }\n if (code === \"MODULE_NOT_FOUND\" || code === \"ERR_MODULE_NOT_FOUND\") {\n return \"contains an unresolved import\";\n }\n return \"threw while loading\";\n}\n\n/** Empty augmentation used when no valid database schema is available. */\nexport const NEUTRAL_DATABASE_TYPES = `// Auto-generated by AppKit - DO NOT EDIT\nimport \"@databricks/appkit\";\n\ndeclare module \"@databricks/appkit\" {\n interface DatabaseRegistry {}\n}\n`;\n\n/** Render one `DatabaseRegistry` augmentation from a finalized schema. */\nfunction render(schema: Schema): string {\n const entries = walkSchema(schema)\n .map(\n (entry) => ` ${JSON.stringify(entry.name)}: {\n row: ${entry.row};\n publicRow: ${entry.publicRow};\n insert: ${entry.insert};\n update: ${entry.update};\n filters: ${entry.filters};\n includes: ${entry.includes};\n hasPrimaryKey: ${entry.hasPrimaryKey};\n };`,\n )\n .join(\"\\n\");\n return `// Auto-generated by AppKit - DO NOT EDIT\nimport \"@databricks/appkit\";\n\ndeclare module \"@databricks/appkit\" {\n type DatabaseLogicalFilter<T> = T & {\n and?: readonly DatabaseLogicalFilter<T>[];\n or?: readonly DatabaseLogicalFilter<T>[];\n };\n\n interface DatabaseRegistry {\n${entries}\n }\n}\n`;\n}\n\n/** Atomically replace generated output only when its content changes. */\nasync function writeIfChanged(outFile: string, content: string): Promise<void> {\n if (\n fsSync.existsSync(outFile) &&\n (await fs.readFile(outFile, \"utf8\")) === content\n )\n return;\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n const temporary = path.join(\n path.dirname(outFile),\n `.${path.basename(outFile)}.${randomUUID()}.tmp`,\n );\n try {\n await fs.writeFile(temporary, content, \"utf8\");\n await fs.rename(temporary, outFile);\n } finally {\n await fs.rm(temporary, { force: true });\n }\n}\n\n/** Disable Jiti's module cache so watch-mode runs observe schema edits. */\nasync function importSchema(schemaFile: string): Promise<unknown> {\n const { createJiti } = await import(\"jiti\");\n return createJiti(import.meta.url, { moduleCache: false }).import(schemaFile);\n}\n\n/** Generate current registry types or neutralize them when loading fails. */\nexport async function generateDatabaseTypes(\n options: GenerateDatabaseTypesOptions,\n): Promise<void> {\n if (!fsSync.existsSync(options.schemaFile)) {\n await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);\n return;\n }\n try {\n const module = (await importSchema(options.schemaFile)) as {\n schema?: Schema;\n };\n if (!(\"schema\" in module)) {\n throw new DatabaseTypegenError(\n `Database schema module \"${schemaLabel(options.schemaFile)}\" must export a named \"schema\"`,\n );\n }\n try {\n assertFinalizedSchema(module.schema);\n } catch {\n throw new DatabaseTypegenError(\n `Database schema \"${schemaLabel(options.schemaFile)}\" is not a finalized AppKit schema`,\n );\n }\n await writeIfChanged(options.outFile, render(module.schema));\n } catch (error) {\n // A failed run must not leave stale entities visible to TypeScript.\n await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);\n throw error instanceof DatabaseTypegenError\n ? error\n : new DatabaseTypegenError(\n `Database schema \"${schemaLabel(options.schemaFile)}\" ${importFailureReason(error)}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,YAAY,UAAU,qCAAqC;AACzD,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAShB,MAAa,sBAAsB;AAEnC,SAAS,YAAY,YAA4B;AAC/C,QAAO,KAAK,SAAS,QAAQ,KAAK,EAAE,WAAW,IAAI;;AAGrD,SAAS,oBAAoB,OAAwB;AACnD,KAAI,iBAAiB,YAAa,QAAO;CACzC,IAAI;AACJ,KAAI;AACF,SACE,SAAS,OAAO,UAAU,WACtB,QAAQ,IAAI,OAAO,OAAO,GAC1B;SACA;AACN,SAAO;;AAET,KAAI,SAAS,sBAAsB,SAAS,uBAC1C,QAAO;AAET,QAAO;;;AAIT,MAAa,yBAAyB;;;;;;;;AAStC,SAAS,OAAO,QAAwB;AActC,QAAO;;;;;;;;;;EAbS,WAAW,OAAO,CAC/B,KACE,UAAU,OAAO,KAAK,UAAU,MAAM,KAAK,CAAC;aACtC,MAAM,IAAI;mBACJ,MAAM,UAAU;gBACnB,MAAM,OAAO;gBACb,MAAM,OAAO;iBACZ,MAAM,QAAQ;kBACb,MAAM,SAAS;uBACV,MAAM,cAAc;QAEtC,CACA,KAAK,KAAK,CAWL;;;;;;AAOV,eAAe,eAAe,SAAiB,SAAgC;AAC7E,
|
|
1
|
+
{"version":3,"file":"generate.js","names":["fsSync"],"sources":["../../../src/type-generator/database/generate.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport fsSync from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { Schema } from \"../../database/schema-builder\";\nimport { assertFinalizedSchema } from \"../../database/schema-builder/define-schema\";\nimport { walkSchema } from \"./walk-schema\";\n\n/** Safe diagnostic raised when database declarations cannot be generated. */\nexport class DatabaseTypegenError extends Error {\n constructor(message = \"Database schema generation failed\") {\n super(message);\n this.name = \"DatabaseTypegenError\";\n }\n}\n\ninterface GenerateDatabaseTypesOptions {\n readonly schemaFile: string;\n readonly outFile: string;\n}\n\nexport const DATABASE_TYPES_FILE = \"database.d.ts\";\n\nfunction schemaLabel(schemaFile: string): string {\n return path.relative(process.cwd(), schemaFile) || schemaFile;\n}\n\nfunction importFailureReason(error: unknown): string {\n if (error instanceof SyntaxError) return \"could not be parsed\";\n let code: unknown;\n try {\n code =\n error && typeof error === \"object\"\n ? Reflect.get(error, \"code\")\n : undefined;\n } catch {\n code = undefined;\n }\n if (code === \"MODULE_NOT_FOUND\" || code === \"ERR_MODULE_NOT_FOUND\") {\n return \"contains an unresolved import\";\n }\n return \"threw while loading\";\n}\n\n/** Empty augmentation used when no valid database schema is available. */\nexport const NEUTRAL_DATABASE_TYPES = `// Auto-generated by AppKit - DO NOT EDIT\nimport \"@databricks/appkit\";\n\ndeclare module \"@databricks/appkit\" {\n interface DatabaseRegistry {}\n}\n`;\n\n/** Render one `DatabaseRegistry` augmentation from a finalized schema. */\nfunction render(schema: Schema): string {\n const entries = walkSchema(schema)\n .map(\n (entry) => ` ${JSON.stringify(entry.name)}: {\n row: ${entry.row};\n publicRow: ${entry.publicRow};\n insert: ${entry.insert};\n update: ${entry.update};\n filters: ${entry.filters};\n includes: ${entry.includes};\n hasPrimaryKey: ${entry.hasPrimaryKey};\n };`,\n )\n .join(\"\\n\");\n return `// Auto-generated by AppKit - DO NOT EDIT\nimport \"@databricks/appkit\";\n\ndeclare module \"@databricks/appkit\" {\n type DatabaseLogicalFilter<T> = T & {\n and?: readonly DatabaseLogicalFilter<T>[];\n or?: readonly DatabaseLogicalFilter<T>[];\n };\n\n interface DatabaseRegistry {\n${entries}\n }\n}\n`;\n}\n\n/** Atomically replace generated output only when its content changes. */\nasync function writeIfChanged(outFile: string, content: string): Promise<void> {\n if (\n fsSync.existsSync(outFile) &&\n (await fs.readFile(outFile, \"utf8\")) === content\n )\n return;\n await fs.mkdir(path.dirname(outFile), { recursive: true });\n const temporary = path.join(\n path.dirname(outFile),\n `.${path.basename(outFile)}.${randomUUID()}.tmp`,\n );\n try {\n await fs.writeFile(temporary, content, \"utf8\");\n await fs.rename(temporary, outFile);\n } finally {\n await fs.rm(temporary, { force: true });\n }\n}\n\n/** Disable Jiti's module cache so watch-mode runs observe schema edits. */\nasync function importSchema(schemaFile: string): Promise<unknown> {\n const { createJiti } = await import(\"jiti\");\n return createJiti(import.meta.url, { moduleCache: false }).import(schemaFile);\n}\n\n/** Generate current registry types or neutralize them when loading fails. */\nexport async function generateDatabaseTypes(\n options: GenerateDatabaseTypesOptions,\n): Promise<void> {\n if (!fsSync.existsSync(options.schemaFile)) {\n await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);\n return;\n }\n try {\n const module = (await importSchema(options.schemaFile)) as {\n schema?: Schema;\n };\n if (!(\"schema\" in module)) {\n throw new DatabaseTypegenError(\n `Database schema module \"${schemaLabel(options.schemaFile)}\" must export a named \"schema\"`,\n );\n }\n try {\n assertFinalizedSchema(module.schema);\n } catch {\n throw new DatabaseTypegenError(\n `Database schema \"${schemaLabel(options.schemaFile)}\" is not a finalized AppKit schema`,\n );\n }\n await writeIfChanged(options.outFile, render(module.schema));\n } catch (error) {\n // A failed run must not leave stale entities visible to TypeScript.\n await writeIfChanged(options.outFile, NEUTRAL_DATABASE_TYPES);\n throw error instanceof DatabaseTypegenError\n ? error\n : new DatabaseTypegenError(\n `Database schema \"${schemaLabel(options.schemaFile)}\" ${importFailureReason(error)}`,\n );\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAa,uBAAb,cAA0C,MAAM;CAC9C,YAAY,UAAU,qCAAqC;AACzD,QAAM,QAAQ;AACd,OAAK,OAAO;;;AAShB,MAAa,sBAAsB;AAEnC,SAAS,YAAY,YAA4B;AAC/C,QAAO,KAAK,SAAS,QAAQ,KAAK,EAAE,WAAW,IAAI;;AAGrD,SAAS,oBAAoB,OAAwB;AACnD,KAAI,iBAAiB,YAAa,QAAO;CACzC,IAAI;AACJ,KAAI;AACF,SACE,SAAS,OAAO,UAAU,WACtB,QAAQ,IAAI,OAAO,OAAO,GAC1B;SACA;AACN,SAAO;;AAET,KAAI,SAAS,sBAAsB,SAAS,uBAC1C,QAAO;AAET,QAAO;;;AAIT,MAAa,yBAAyB;;;;;;;;AAStC,SAAS,OAAO,QAAwB;AActC,QAAO;;;;;;;;;;EAbS,WAAW,OAAO,CAC/B,KACE,UAAU,OAAO,KAAK,UAAU,MAAM,KAAK,CAAC;aACtC,MAAM,IAAI;mBACJ,MAAM,UAAU;gBACnB,MAAM,OAAO;gBACb,MAAM,OAAO;iBACZ,MAAM,QAAQ;kBACb,MAAM,SAAS;uBACV,MAAM,cAAc;QAEtC,CACA,KAAK,KAAK,CAWL;;;;;;AAOV,eAAe,eAAe,SAAiB,SAAgC;AAC7E,KACEA,KAAO,WAAW,QAAQ,IACzB,MAAM,GAAG,SAAS,SAAS,OAAO,KAAM,QAEzC;AACF,OAAM,GAAG,MAAM,KAAK,QAAQ,QAAQ,EAAE,EAAE,WAAW,MAAM,CAAC;CAC1D,MAAM,YAAY,KAAK,KACrB,KAAK,QAAQ,QAAQ,EACrB,IAAI,KAAK,SAAS,QAAQ,CAAC,GAAG,YAAY,CAAC,MAC5C;AACD,KAAI;AACF,QAAM,GAAG,UAAU,WAAW,SAAS,OAAO;AAC9C,QAAM,GAAG,OAAO,WAAW,QAAQ;WAC3B;AACR,QAAM,GAAG,GAAG,WAAW,EAAE,OAAO,MAAM,CAAC;;;;AAK3C,eAAe,aAAa,YAAsC;CAChE,MAAM,EAAE,eAAe,MAAM,OAAO;AACpC,QAAO,WAAW,OAAO,KAAK,KAAK,EAAE,aAAa,OAAO,CAAC,CAAC,OAAO,WAAW;;;AAI/E,eAAsB,sBACpB,SACe;AACf,KAAI,CAACA,KAAO,WAAW,QAAQ,WAAW,EAAE;AAC1C,QAAM,eAAe,QAAQ,SAAS,uBAAuB;AAC7D;;AAEF,KAAI;EACF,MAAM,SAAU,MAAM,aAAa,QAAQ,WAAW;AAGtD,MAAI,EAAE,YAAY,QAChB,OAAM,IAAI,qBACR,2BAA2B,YAAY,QAAQ,WAAW,CAAC,gCAC5D;AAEH,MAAI;AACF,yBAAsB,OAAO,OAAO;UAC9B;AACN,SAAM,IAAI,qBACR,oBAAoB,YAAY,QAAQ,WAAW,CAAC,oCACrD;;AAEH,QAAM,eAAe,QAAQ,SAAS,OAAO,OAAO,OAAO,CAAC;UACrD,OAAO;AAEd,QAAM,eAAe,QAAQ,SAAS,uBAAuB;AAC7D,QAAM,iBAAiB,uBACnB,QACA,IAAI,qBACF,oBAAoB,YAAY,QAAQ,WAAW,CAAC,IAAI,oBAAoB,MAAM,GACnF"}
|
|
@@ -124,19 +124,34 @@ function classifyBlockingFailure(error) {
|
|
|
124
124
|
}
|
|
125
125
|
return "environmental";
|
|
126
126
|
}
|
|
127
|
+
const AUTH_ERROR_CODES = new Set(["PERMISSION_DENIED", "UNAUTHENTICATED"]);
|
|
127
128
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
129
|
+
* Extract a Databricks `error_code` (e.g. "PERMISSION_DENIED") from a thrown
|
|
130
|
+
* error. The SDK surfaces it either as a top-level `error_code` field or as a
|
|
131
|
+
* JSON body embedded in the message string, e.g.
|
|
132
|
+
* `Response from server (Forbidden) {"error_code":"PERMISSION_DENIED",...}`.
|
|
133
|
+
*/
|
|
134
|
+
function getDatabricksErrorCode(error) {
|
|
135
|
+
if (!isObject(error)) return void 0;
|
|
136
|
+
if (typeof error.error_code === "string") return error.error_code;
|
|
137
|
+
const jsonMatch = (typeof error.message === "string" ? error.message : void 0)?.match(/\{[\s\S]*\}/);
|
|
138
|
+
if (jsonMatch) try {
|
|
139
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
140
|
+
if (typeof parsed.error_code === "string") return parsed.error_code;
|
|
141
|
+
} catch {}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* True when a thrown failure is an authentication/authorization problem: an
|
|
145
|
+
* HTTP 401/403, or a Databricks `error_code` of PERMISSION_DENIED /
|
|
146
|
+
* UNAUTHENTICATED (which can arrive with no numeric status). Walks
|
|
147
|
+
* `cause`/`AggregateError` chains so a wrapped auth error is still recognized.
|
|
130
148
|
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* -
|
|
134
|
-
*
|
|
135
|
-
* - "unavailable": everything else (DELETED/DELETING, wait timeouts, degraded
|
|
136
|
-
* DESCRIBEs).
|
|
149
|
+
* Callers degrade rather than fail the build on `true`: a build-time identity
|
|
150
|
+
* gap — the build runs as a different principal than the app's runtime
|
|
151
|
+
* on-behalf-of user — must not block a deploy when committed types exist. The
|
|
152
|
+
* has-types gate still crashes a fresh checkout with nothing to fall back to.
|
|
137
153
|
*/
|
|
138
|
-
function
|
|
139
|
-
if (isConnectivityError(error)) return "unreachable";
|
|
154
|
+
function isAuthError(error) {
|
|
140
155
|
const seen = /* @__PURE__ */ new Set();
|
|
141
156
|
const stack = [error];
|
|
142
157
|
while (stack.length > 0) {
|
|
@@ -144,9 +159,28 @@ function classifyEnvironmentalCause(error) {
|
|
|
144
159
|
if (current === void 0 || seen.has(current)) continue;
|
|
145
160
|
seen.add(current);
|
|
146
161
|
const status = getErrorStatus(current);
|
|
147
|
-
if (status !== void 0 && AUTH_ERROR_STATUSES.has(status)) return
|
|
162
|
+
if (status !== void 0 && AUTH_ERROR_STATUSES.has(status)) return true;
|
|
163
|
+
const code = getDatabricksErrorCode(current);
|
|
164
|
+
if (code && AUTH_ERROR_CODES.has(code)) return true;
|
|
148
165
|
stack.push(...getErrorChildren(current));
|
|
149
166
|
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Coarse cause label for an environmental failure, used by the `--wait`
|
|
171
|
+
* committed-types warning so the log says *why* generation fell back.
|
|
172
|
+
*
|
|
173
|
+
* Returns:
|
|
174
|
+
* - "unreachable": transport/connectivity failure (see {@link isConnectivityError}).
|
|
175
|
+
* - "auth": HTTP 401/403 or a PERMISSION_DENIED / UNAUTHENTICATED `error_code`,
|
|
176
|
+
* including one carried on `response.status` or wrapped in a
|
|
177
|
+
* `cause`/`AggregateError` chain (see {@link isAuthError}).
|
|
178
|
+
* - "unavailable": everything else (DELETED/DELETING, wait timeouts, degraded
|
|
179
|
+
* DESCRIBEs).
|
|
180
|
+
*/
|
|
181
|
+
function classifyEnvironmentalCause(error) {
|
|
182
|
+
if (isConnectivityError(error)) return "unreachable";
|
|
183
|
+
if (isAuthError(error)) return "auth";
|
|
150
184
|
return "unavailable";
|
|
151
185
|
}
|
|
152
186
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.js","names":[],"sources":["../../src/type-generator/errors.ts"],"sourcesContent":["/**\n * Shared error-introspection utilities for the type generator.\n *\n * Both describe paths — the query registry and the metric-view registry — must\n * classify a thrown failure the same way: a transient connectivity blip (which\n * self-converges and should never fail a build) versus a deterministic failure\n * (auth, bad warehouse id, truncated result, malformed request) that must be\n * surfaced. Keeping `isConnectivityError` (and the message/diagnostic helpers it\n * shares) in one module is the single source of truth for that decision, so the\n * two paths can never drift apart.\n */\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n if (isObject(error) && typeof error.message === \"string\") {\n return error.message;\n }\n return String(error);\n}\n\nexport function getErrorDiagnostic(error: unknown): string {\n const seen = new Set<unknown>();\n const messages: string[] = [];\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const message = getErrorMessage(current);\n if (\n message &&\n message !== \"[object Object]\" &&\n !messages.includes(message)\n ) {\n messages.push(message);\n }\n\n const code = getErrorCode(current);\n if (code && !messages.includes(code)) messages.push(code);\n\n stack.push(...getErrorChildren(current));\n }\n\n return messages.length > 0 ? messages.join(\": \") : getErrorMessage(error);\n}\n\nfunction getErrorCode(error: unknown): string | undefined {\n if (!isObject(error)) return undefined;\n const code = error.code ?? error.errno;\n return typeof code === \"string\" ? code : undefined;\n}\n\nfunction getErrorStatus(error: unknown): number | undefined {\n if (!isObject(error)) return undefined;\n const direct = error.status ?? error.statusCode;\n if (typeof direct === \"number\") return direct;\n if (isObject(error.response) && typeof error.response.status === \"number\") {\n return error.response.status;\n }\n return undefined;\n}\n\nfunction getErrorChildren(error: unknown): unknown[] {\n if (!isObject(error)) return [];\n const children: unknown[] = [];\n if (\"cause\" in error) children.push(error.cause);\n if (error instanceof AggregateError) children.push(...error.errors);\n return children;\n}\n\nconst CONNECTIVITY_ERROR_CODES = new Set([\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENOTFOUND\",\n \"ETIMEDOUT\",\n \"EAI_AGAIN\",\n \"EAI_NODATA\",\n \"EAI_NONAME\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"CERT_HAS_EXPIRED\",\n \"DEPTH_ZERO_SELF_SIGNED_CERT\",\n \"ERR_TLS_CERT_ALTNAME_INVALID\",\n \"SELF_SIGNED_CERT_IN_CHAIN\",\n \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\",\n]);\n\nfunction isConnectivityMessage(message: string): boolean {\n return (\n /\\bconnection (?:refused|reset|timed out)\\b/i.test(message) ||\n /\\bsocket hang up\\b/i.test(message) ||\n /\\bnetwork error\\b/i.test(message) ||\n /\\bcan'?t connect to\\b/i.test(message) ||\n /\\bcertificate has expired\\b/i.test(message) ||\n /\\bunable to verify the first certificate\\b/i.test(message) ||\n /\\bupstream connect error or disconnect\\/reset before headers\\b/i.test(\n message,\n )\n );\n}\n\n/**\n * True when a thrown failure is a transport/connectivity problem (DNS, refused\n * connection, reset, TLS, 502/503/504, undici `UND_ERR_*`) rather than a\n * deterministic error the warehouse returned. Walks `cause`/`AggregateError`\n * chains so a wrapped connectivity error is still recognized. Callers degrade\n * (and retry) on `true`; everything else is surfaced as a build failure.\n */\nexport function isConnectivityError(error: unknown): boolean {\n const seen = new Set<unknown>();\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const code = getErrorCode(current);\n if (\n code &&\n (CONNECTIVITY_ERROR_CODES.has(code) || code.startsWith(\"UND_ERR_\"))\n ) {\n return true;\n }\n\n const status = getErrorStatus(current);\n if (status === 502 || status === 503 || status === 504) return true;\n\n if (isConnectivityMessage(getErrorMessage(current))) return true;\n\n stack.push(...getErrorChildren(current));\n }\n\n return false;\n}\n\nconst AUTH_ERROR_STATUSES = new Set([401, 403]);\n\n/**\n * Classifies a thrown failure into one of two buckets: deterministic failures\n * that must be surfaced (bad warehouse id, malformed request) or environmental\n * issues (connectivity, auth, deleted warehouse, timeouts) that the has-types\n * gate will handle later.\n *\n * Returns:\n * - \"deterministic\": HTTP 404 (bad warehouse id) or 400 (malformed request).\n * The build must fail.\n * - \"environmental\": Everything else — auth (401/403), connectivity errors,\n * warehouse state changes (DELETED/DELETING), wait-for-RUNNING timeouts,\n * unrecognized failures. Default = environmental.\n *\n * Walks `cause`/`AggregateError` chains when checking for deterministic status,\n * so a wrapped 404 is still recognized as deterministic.\n */\nexport function classifyBlockingFailure(\n error: unknown,\n): \"deterministic\" | \"environmental\" {\n const seen = new Set<unknown>();\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const status = getErrorStatus(current);\n if (status === 404 || status === 400) {\n return \"deterministic\";\n }\n\n stack.push(...getErrorChildren(current));\n }\n\n return \"environmental\";\n}\n\n/**\n * Coarse cause label for an environmental failure, used by the `--wait`\n * committed-types warning so the log says *why* generation fell back.\n *\n * Returns:\n * - \"unreachable\": transport/connectivity failure (see {@link isConnectivityError}).\n * - \"auth\": HTTP 401/403, including a status carried on `response.status` or\n * wrapped in a `cause`/`AggregateError` chain.\n * - \"unavailable\": everything else (DELETED/DELETING, wait timeouts, degraded\n * DESCRIBEs).\n */\nexport function classifyEnvironmentalCause(\n error: unknown,\n): \"auth\" | \"unreachable\" | \"unavailable\" {\n if (isConnectivityError(error)) return \"unreachable\";\n\n // Walk the error chain so a wrapped 401/403 is still labeled as auth.\n const seen = new Set<unknown>();\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const status = getErrorStatus(current);\n if (status !== undefined && AUTH_ERROR_STATUSES.has(status)) return \"auth\";\n\n stack.push(...getErrorChildren(current));\n }\n\n return \"unavailable\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../../src/type-generator/errors.ts"],"sourcesContent":["/**\n * Shared error-introspection utilities for the type generator.\n *\n * Both describe paths — the query registry and the metric-view registry — must\n * classify a thrown failure the same way: a transient connectivity blip (which\n * self-converges and should never fail a build) versus a deterministic failure\n * (auth, bad warehouse id, truncated result, malformed request) that must be\n * surfaced. Keeping `isConnectivityError` (and the message/diagnostic helpers it\n * shares) in one module is the single source of truth for that decision, so the\n * two paths can never drift apart.\n */\n\nfunction isObject(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n if (isObject(error) && typeof error.message === \"string\") {\n return error.message;\n }\n return String(error);\n}\n\nexport function getErrorDiagnostic(error: unknown): string {\n const seen = new Set<unknown>();\n const messages: string[] = [];\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const message = getErrorMessage(current);\n if (\n message &&\n message !== \"[object Object]\" &&\n !messages.includes(message)\n ) {\n messages.push(message);\n }\n\n const code = getErrorCode(current);\n if (code && !messages.includes(code)) messages.push(code);\n\n stack.push(...getErrorChildren(current));\n }\n\n return messages.length > 0 ? messages.join(\": \") : getErrorMessage(error);\n}\n\nfunction getErrorCode(error: unknown): string | undefined {\n if (!isObject(error)) return undefined;\n const code = error.code ?? error.errno;\n return typeof code === \"string\" ? code : undefined;\n}\n\nfunction getErrorStatus(error: unknown): number | undefined {\n if (!isObject(error)) return undefined;\n const direct = error.status ?? error.statusCode;\n if (typeof direct === \"number\") return direct;\n if (isObject(error.response) && typeof error.response.status === \"number\") {\n return error.response.status;\n }\n return undefined;\n}\n\nfunction getErrorChildren(error: unknown): unknown[] {\n if (!isObject(error)) return [];\n const children: unknown[] = [];\n if (\"cause\" in error) children.push(error.cause);\n if (error instanceof AggregateError) children.push(...error.errors);\n return children;\n}\n\nconst CONNECTIVITY_ERROR_CODES = new Set([\n \"ECONNREFUSED\",\n \"ECONNRESET\",\n \"ENOTFOUND\",\n \"ETIMEDOUT\",\n \"EAI_AGAIN\",\n \"EAI_NODATA\",\n \"EAI_NONAME\",\n \"EHOSTUNREACH\",\n \"ENETUNREACH\",\n \"CERT_HAS_EXPIRED\",\n \"DEPTH_ZERO_SELF_SIGNED_CERT\",\n \"ERR_TLS_CERT_ALTNAME_INVALID\",\n \"SELF_SIGNED_CERT_IN_CHAIN\",\n \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\",\n]);\n\nfunction isConnectivityMessage(message: string): boolean {\n return (\n /\\bconnection (?:refused|reset|timed out)\\b/i.test(message) ||\n /\\bsocket hang up\\b/i.test(message) ||\n /\\bnetwork error\\b/i.test(message) ||\n /\\bcan'?t connect to\\b/i.test(message) ||\n /\\bcertificate has expired\\b/i.test(message) ||\n /\\bunable to verify the first certificate\\b/i.test(message) ||\n /\\bupstream connect error or disconnect\\/reset before headers\\b/i.test(\n message,\n )\n );\n}\n\n/**\n * True when a thrown failure is a transport/connectivity problem (DNS, refused\n * connection, reset, TLS, 502/503/504, undici `UND_ERR_*`) rather than a\n * deterministic error the warehouse returned. Walks `cause`/`AggregateError`\n * chains so a wrapped connectivity error is still recognized. Callers degrade\n * (and retry) on `true`; everything else is surfaced as a build failure.\n */\nexport function isConnectivityError(error: unknown): boolean {\n const seen = new Set<unknown>();\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const code = getErrorCode(current);\n if (\n code &&\n (CONNECTIVITY_ERROR_CODES.has(code) || code.startsWith(\"UND_ERR_\"))\n ) {\n return true;\n }\n\n const status = getErrorStatus(current);\n if (status === 502 || status === 503 || status === 504) return true;\n\n if (isConnectivityMessage(getErrorMessage(current))) return true;\n\n stack.push(...getErrorChildren(current));\n }\n\n return false;\n}\n\nconst AUTH_ERROR_STATUSES = new Set([401, 403]);\n\n/**\n * Classifies a thrown failure into one of two buckets: deterministic failures\n * that must be surfaced (bad warehouse id, malformed request) or environmental\n * issues (connectivity, auth, deleted warehouse, timeouts) that the has-types\n * gate will handle later.\n *\n * Returns:\n * - \"deterministic\": HTTP 404 (bad warehouse id) or 400 (malformed request).\n * The build must fail.\n * - \"environmental\": Everything else — auth (401/403), connectivity errors,\n * warehouse state changes (DELETED/DELETING), wait-for-RUNNING timeouts,\n * unrecognized failures. Default = environmental.\n *\n * Walks `cause`/`AggregateError` chains when checking for deterministic status,\n * so a wrapped 404 is still recognized as deterministic.\n */\nexport function classifyBlockingFailure(\n error: unknown,\n): \"deterministic\" | \"environmental\" {\n const seen = new Set<unknown>();\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const status = getErrorStatus(current);\n if (status === 404 || status === 400) {\n return \"deterministic\";\n }\n\n stack.push(...getErrorChildren(current));\n }\n\n return \"environmental\";\n}\n\n// Databricks REST/SDK auth error codes. These arrive as an `error_code` string\n// (a top-level field, or a JSON body embedded in the message) rather than a\n// numeric HTTP status, so status-only detection misses them.\nconst AUTH_ERROR_CODES = new Set([\"PERMISSION_DENIED\", \"UNAUTHENTICATED\"]);\n\n/**\n * Extract a Databricks `error_code` (e.g. \"PERMISSION_DENIED\") from a thrown\n * error. The SDK surfaces it either as a top-level `error_code` field or as a\n * JSON body embedded in the message string, e.g.\n * `Response from server (Forbidden) {\"error_code\":\"PERMISSION_DENIED\",...}`.\n */\nfunction getDatabricksErrorCode(error: unknown): string | undefined {\n if (!isObject(error)) return undefined;\n if (typeof error.error_code === \"string\") return error.error_code;\n\n const message = typeof error.message === \"string\" ? error.message : undefined;\n const jsonMatch = message?.match(/\\{[\\s\\S]*\\}/);\n if (jsonMatch) {\n try {\n const parsed = JSON.parse(jsonMatch[0]) as { error_code?: unknown };\n if (typeof parsed.error_code === \"string\") return parsed.error_code;\n } catch {\n // not valid JSON — fall through\n }\n }\n return undefined;\n}\n\n/**\n * True when a thrown failure is an authentication/authorization problem: an\n * HTTP 401/403, or a Databricks `error_code` of PERMISSION_DENIED /\n * UNAUTHENTICATED (which can arrive with no numeric status). Walks\n * `cause`/`AggregateError` chains so a wrapped auth error is still recognized.\n *\n * Callers degrade rather than fail the build on `true`: a build-time identity\n * gap — the build runs as a different principal than the app's runtime\n * on-behalf-of user — must not block a deploy when committed types exist. The\n * has-types gate still crashes a fresh checkout with nothing to fall back to.\n */\nexport function isAuthError(error: unknown): boolean {\n const seen = new Set<unknown>();\n const stack = [error];\n\n while (stack.length > 0) {\n const current = stack.pop();\n if (current === undefined || seen.has(current)) continue;\n seen.add(current);\n\n const status = getErrorStatus(current);\n if (status !== undefined && AUTH_ERROR_STATUSES.has(status)) return true;\n\n const code = getDatabricksErrorCode(current);\n if (code && AUTH_ERROR_CODES.has(code)) return true;\n\n stack.push(...getErrorChildren(current));\n }\n\n return false;\n}\n\n/**\n * Coarse cause label for an environmental failure, used by the `--wait`\n * committed-types warning so the log says *why* generation fell back.\n *\n * Returns:\n * - \"unreachable\": transport/connectivity failure (see {@link isConnectivityError}).\n * - \"auth\": HTTP 401/403 or a PERMISSION_DENIED / UNAUTHENTICATED `error_code`,\n * including one carried on `response.status` or wrapped in a\n * `cause`/`AggregateError` chain (see {@link isAuthError}).\n * - \"unavailable\": everything else (DELETED/DELETING, wait timeouts, degraded\n * DESCRIBEs).\n */\nexport function classifyEnvironmentalCause(\n error: unknown,\n): \"auth\" | \"unreachable\" | \"unavailable\" {\n if (isConnectivityError(error)) return \"unreachable\";\n if (isAuthError(error)) return \"auth\";\n return \"unavailable\";\n}\n"],"mappings":"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"}
|
|
@@ -2,7 +2,7 @@ import { createLogger } from "../logging/logger.js";
|
|
|
2
2
|
import pc from "picocolors";
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import
|
|
5
|
+
import fs$1 from "node:fs";
|
|
6
6
|
|
|
7
7
|
//#region src/type-generator/migration.ts
|
|
8
8
|
const logger = createLogger("type-generator:migration");
|
|
@@ -16,7 +16,7 @@ const logger = createLogger("type-generator:migration");
|
|
|
16
16
|
*/
|
|
17
17
|
function resolveProjectRoot(outFile) {
|
|
18
18
|
const root = path.resolve(path.dirname(outFile), "..", "..");
|
|
19
|
-
if (!
|
|
19
|
+
if (!fs$1.existsSync(path.join(root, "package.json"))) logger.warn("Resolved project root %s has no package.json — migration may target the wrong directory. Check your outFile path: %s", root, outFile);
|
|
20
20
|
return root;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.js","names":[],"sources":["../../src/type-generator/migration.ts"],"sourcesContent":["import fsSync from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport pc from \"picocolors\";\n\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"type-generator:migration\");\n\n/**\n * Derive project root from an outFile path.\n * outFile is always `<projectRoot>/shared/appkit-types/<file>` — both the Vite plugins\n * and the CLI construct it this way, so going up two levels is safe.\n *\n * Validates that the resolved root contains a package.json — if not, logs a warning\n * so custom outFile paths don't silently operate on the wrong directory.\n */\nexport function resolveProjectRoot(outFile: string): string {\n const root = path.resolve(path.dirname(outFile), \"..\", \"..\");\n if (!fsSync.existsSync(path.join(root, \"package.json\"))) {\n logger.warn(\n \"Resolved project root %s has no package.json — migration may target the wrong directory. \" +\n \"Check your outFile path: %s\",\n root,\n outFile,\n );\n }\n return root;\n}\n\n/**\n * Remove old generated types from client/src/ (pre-shared/ location).\n * Best-effort: silently ignores missing files.\n */\nexport async function removeOldGeneratedTypes(\n projectRoot: string,\n filename: string,\n): Promise<void> {\n const oldFile = path.join(projectRoot, \"client\", \"src\", filename);\n try {\n await fs.unlink(oldFile);\n logger.debug(\"Removed old types at %s\", oldFile);\n } catch {\n // File doesn't exist — nothing to clean up\n }\n}\n\n// ── Project config migration ────────────────────────────────────────────\n\nconst migratedProjects = new Set<string>();\n\n/**\n * One-time config migration: update tsconfig and package.json for shared/ types output.\n * Idempotent — each sub-migration checks current file state and skips if already migrated.\n * Deduplicates per project root so monorepo builds migrate each app independently.\n * Opt-out: set `\"appkit\": { \"autoMigrate\": false }` in package.json.\n */\nexport async function migrateProjectConfig(projectRoot: string): Promise<void> {\n const resolved = path.resolve(projectRoot);\n if (migratedProjects.has(resolved)) return;\n migratedProjects.add(resolved);\n\n if (await isAutoMigrateDisabled(projectRoot)) {\n logger.debug(\"Auto-migration disabled via package.json appkit.autoMigrate\");\n return;\n }\n\n const results: Array<{ file: string; action: string }> = [];\n\n results.push(...(await migrateTsconfigClient(projectRoot)));\n results.push(...(await migrateTsconfigServer(projectRoot)));\n results.push(...(await migratePackageJsonScripts(projectRoot)));\n\n if (results.length > 0) {\n printMigrationSummary(results);\n }\n}\n\n/** Exported for testing only. */\nexport function _resetMigrationState(): void {\n migratedProjects.clear();\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nasync function isAutoMigrateDisabled(projectRoot: string): Promise<boolean> {\n try {\n const raw = await fs.readFile(\n path.join(projectRoot, \"package.json\"),\n \"utf-8\",\n );\n const parsed = JSON.parse(raw);\n return parsed.appkit?.autoMigrate === false;\n } catch {\n return false;\n }\n}\n\n/** Strip JSONC comments (block and line) so JSON.parse can handle tsconfig files. */\nfunction stripJsonComments(text: string): string {\n // Match strings (to skip them) or comments (to remove them).\n // Strings must be matched first to avoid stripping comment-like patterns inside string values\n // (e.g. \"server/**/*\" contains /* which looks like a block comment start).\n return text.replace(/\"(?:[^\"\\\\]|\\\\.)*\"|\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*/g, (match) =>\n match.startsWith('\"') ? match : \"\",\n );\n}\n\ntype MigrationResult = Array<{ file: string; action: string }>;\n\n// ── tsconfig.client.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigClient(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.client.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n\n if (!Array.isArray(parsed.include)) return results;\n if (parsed.include.includes(\"shared/appkit-types\")) return results;\n\n parsed.include.push(\"shared/appkit-types\");\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.client.json\",\n action: 'added \"shared/appkit-types\" to include',\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.client.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── tsconfig.server.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigServer(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.server.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n const opts = parsed.compilerOptions;\n\n if (!opts || !opts.outDir) return results; // already migrated or non-standard\n\n delete opts.outDir;\n delete opts.declaration;\n delete opts.declarationMap;\n delete opts.sourceMap;\n opts.noEmit = true;\n\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.server.json\",\n action: \"switched to noEmit mode\",\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.server.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── package.json ────────────────────────────────────────────────────────\n\nconst SCRIPT_MIGRATIONS: Record<string, { old: string; new: string }> = {\n \"build:server\": {\n old: \"tsdown -c tsdown.server.config.ts\",\n new: \"tsc -b tsconfig.server.json && tsdown -c tsdown.server.config.ts\",\n },\n typecheck: {\n old: \"tsc -p ./tsconfig.server.json --noEmit && tsc -p ./tsconfig.client.json --noEmit\",\n new: \"tsc -b tsconfig.server.json && tsc -b tsconfig.client.json\",\n },\n};\n\nasync function migratePackageJsonScripts(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"package.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n const scripts = parsed.scripts;\n if (!scripts) return results;\n\n const updated: string[] = [];\n\n for (const [name, { old, new: replacement }] of Object.entries(\n SCRIPT_MIGRATIONS,\n )) {\n if (scripts[name] === old) {\n scripts[name] = replacement;\n updated.push(name);\n }\n }\n\n if (updated.length === 0) return results;\n\n const indent = raw.match(/^\\s+/m)?.[0]?.length === 4 ? 4 : 2;\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, indent)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"package.json\",\n action: `updated ${updated.join(\" and \")} scripts`,\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate package.json scripts: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── Summary ─────────────────────────────────────────────────────────────\n\nfunction printMigrationSummary(\n results: Array<{ file: string; action: string }>,\n) {\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(` ${pc.bold(\"Typegen Migration\")}`);\n console.log(` ${separator}`);\n for (const { file, action } of results) {\n console.log(` ${pc.green(\"✓\")} ${file.padEnd(24)} ${pc.dim(action)}`);\n }\n console.log(` ${separator}`);\n console.log(\"\");\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"migration.js","names":["fsSync"],"sources":["../../src/type-generator/migration.ts"],"sourcesContent":["import fsSync from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport pc from \"picocolors\";\n\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"type-generator:migration\");\n\n/**\n * Derive project root from an outFile path.\n * outFile is always `<projectRoot>/shared/appkit-types/<file>` — both the Vite plugins\n * and the CLI construct it this way, so going up two levels is safe.\n *\n * Validates that the resolved root contains a package.json — if not, logs a warning\n * so custom outFile paths don't silently operate on the wrong directory.\n */\nexport function resolveProjectRoot(outFile: string): string {\n const root = path.resolve(path.dirname(outFile), \"..\", \"..\");\n if (!fsSync.existsSync(path.join(root, \"package.json\"))) {\n logger.warn(\n \"Resolved project root %s has no package.json — migration may target the wrong directory. \" +\n \"Check your outFile path: %s\",\n root,\n outFile,\n );\n }\n return root;\n}\n\n/**\n * Remove old generated types from client/src/ (pre-shared/ location).\n * Best-effort: silently ignores missing files.\n */\nexport async function removeOldGeneratedTypes(\n projectRoot: string,\n filename: string,\n): Promise<void> {\n const oldFile = path.join(projectRoot, \"client\", \"src\", filename);\n try {\n await fs.unlink(oldFile);\n logger.debug(\"Removed old types at %s\", oldFile);\n } catch {\n // File doesn't exist — nothing to clean up\n }\n}\n\n// ── Project config migration ────────────────────────────────────────────\n\nconst migratedProjects = new Set<string>();\n\n/**\n * One-time config migration: update tsconfig and package.json for shared/ types output.\n * Idempotent — each sub-migration checks current file state and skips if already migrated.\n * Deduplicates per project root so monorepo builds migrate each app independently.\n * Opt-out: set `\"appkit\": { \"autoMigrate\": false }` in package.json.\n */\nexport async function migrateProjectConfig(projectRoot: string): Promise<void> {\n const resolved = path.resolve(projectRoot);\n if (migratedProjects.has(resolved)) return;\n migratedProjects.add(resolved);\n\n if (await isAutoMigrateDisabled(projectRoot)) {\n logger.debug(\"Auto-migration disabled via package.json appkit.autoMigrate\");\n return;\n }\n\n const results: Array<{ file: string; action: string }> = [];\n\n results.push(...(await migrateTsconfigClient(projectRoot)));\n results.push(...(await migrateTsconfigServer(projectRoot)));\n results.push(...(await migratePackageJsonScripts(projectRoot)));\n\n if (results.length > 0) {\n printMigrationSummary(results);\n }\n}\n\n/** Exported for testing only. */\nexport function _resetMigrationState(): void {\n migratedProjects.clear();\n}\n\n// ── Helpers ─────────────────────────────────────────────────────────────\n\nasync function isAutoMigrateDisabled(projectRoot: string): Promise<boolean> {\n try {\n const raw = await fs.readFile(\n path.join(projectRoot, \"package.json\"),\n \"utf-8\",\n );\n const parsed = JSON.parse(raw);\n return parsed.appkit?.autoMigrate === false;\n } catch {\n return false;\n }\n}\n\n/** Strip JSONC comments (block and line) so JSON.parse can handle tsconfig files. */\nfunction stripJsonComments(text: string): string {\n // Match strings (to skip them) or comments (to remove them).\n // Strings must be matched first to avoid stripping comment-like patterns inside string values\n // (e.g. \"server/**/*\" contains /* which looks like a block comment start).\n return text.replace(/\"(?:[^\"\\\\]|\\\\.)*\"|\\/\\*[\\s\\S]*?\\*\\/|\\/\\/.*/g, (match) =>\n match.startsWith('\"') ? match : \"\",\n );\n}\n\ntype MigrationResult = Array<{ file: string; action: string }>;\n\n// ── tsconfig.client.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigClient(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.client.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n\n if (!Array.isArray(parsed.include)) return results;\n if (parsed.include.includes(\"shared/appkit-types\")) return results;\n\n parsed.include.push(\"shared/appkit-types\");\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.client.json\",\n action: 'added \"shared/appkit-types\" to include',\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.client.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── tsconfig.server.json ────────────────────────────────────────────────\n\nasync function migrateTsconfigServer(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"tsconfig.server.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(stripJsonComments(raw));\n const opts = parsed.compilerOptions;\n\n if (!opts || !opts.outDir) return results; // already migrated or non-standard\n\n delete opts.outDir;\n delete opts.declaration;\n delete opts.declarationMap;\n delete opts.sourceMap;\n opts.noEmit = true;\n\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, 2)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"tsconfig.server.json\",\n action: \"switched to noEmit mode\",\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate tsconfig.server.json: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── package.json ────────────────────────────────────────────────────────\n\nconst SCRIPT_MIGRATIONS: Record<string, { old: string; new: string }> = {\n \"build:server\": {\n old: \"tsdown -c tsdown.server.config.ts\",\n new: \"tsc -b tsconfig.server.json && tsdown -c tsdown.server.config.ts\",\n },\n typecheck: {\n old: \"tsc -p ./tsconfig.server.json --noEmit && tsc -p ./tsconfig.client.json --noEmit\",\n new: \"tsc -b tsconfig.server.json && tsc -b tsconfig.client.json\",\n },\n};\n\nasync function migratePackageJsonScripts(\n projectRoot: string,\n): Promise<MigrationResult> {\n const results: MigrationResult = [];\n const filePath = path.join(projectRoot, \"package.json\");\n\n try {\n const raw = await fs.readFile(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n const scripts = parsed.scripts;\n if (!scripts) return results;\n\n const updated: string[] = [];\n\n for (const [name, { old, new: replacement }] of Object.entries(\n SCRIPT_MIGRATIONS,\n )) {\n if (scripts[name] === old) {\n scripts[name] = replacement;\n updated.push(name);\n }\n }\n\n if (updated.length === 0) return results;\n\n const indent = raw.match(/^\\s+/m)?.[0]?.length === 4 ? 4 : 2;\n await fs.writeFile(\n filePath,\n `${JSON.stringify(parsed, null, indent)}\\n`,\n \"utf-8\",\n );\n results.push({\n file: \"package.json\",\n action: `updated ${updated.join(\" and \")} scripts`,\n });\n } catch (err) {\n logger.warn(\n \"Failed to migrate package.json scripts: %s\",\n (err as Error).message,\n );\n }\n\n return results;\n}\n\n// ── Summary ─────────────────────────────────────────────────────────────\n\nfunction printMigrationSummary(\n results: Array<{ file: string; action: string }>,\n) {\n const separator = pc.dim(\"─\".repeat(50));\n console.log(\"\");\n console.log(` ${pc.bold(\"Typegen Migration\")}`);\n console.log(` ${separator}`);\n for (const { file, action } of results) {\n console.log(` ${pc.green(\"✓\")} ${file.padEnd(24)} ${pc.dim(action)}`);\n }\n console.log(` ${separator}`);\n console.log(\"\");\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { classifyBlockingFailure, getErrorDiagnostic } from "../errors.js";
|
|
2
2
|
import { extractMetricColumns, parseDescribeTableExtendedJson } from "./describe.js";
|
|
3
3
|
|
|
4
4
|
//#region src/type-generator/mv-registry/sync.ts
|
|
@@ -40,7 +40,7 @@ async function syncMetrics(resolution, fetcher) {
|
|
|
40
40
|
try {
|
|
41
41
|
response = await fetcher(entry.source);
|
|
42
42
|
} catch (err) {
|
|
43
|
-
return failedOutcome(index, entry, `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(err)}`,
|
|
43
|
+
return failedOutcome(index, entry, `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(err)}`, classifyBlockingFailure(err) !== "deterministic");
|
|
44
44
|
}
|
|
45
45
|
const state = response.status?.state;
|
|
46
46
|
if (state !== "SUCCEEDED" && state !== "FAILED") return {
|
|
@@ -79,7 +79,7 @@ async function syncMetrics(resolution, fetcher) {
|
|
|
79
79
|
} else {
|
|
80
80
|
const index = offset + i;
|
|
81
81
|
const entry = entries[index];
|
|
82
|
-
const { schema, failure } = failedOutcome(index, entry, `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(result.reason)}`,
|
|
82
|
+
const { schema, failure } = failedOutcome(index, entry, `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(result.reason)}`, classifyBlockingFailure(result.reason) !== "deterministic");
|
|
83
83
|
schemas[index] = schema;
|
|
84
84
|
failureSlots[index] = failure;
|
|
85
85
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.js","names":[],"sources":["../../../src/type-generator/mv-registry/sync.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"sync.js","names":[],"sources":["../../../src/type-generator/mv-registry/sync.ts"],"sourcesContent":["import { classifyBlockingFailure, getErrorDiagnostic } from \"../errors\";\nimport type { DatabricksStatementExecutionResponse } from \"../types\";\nimport {\n extractMetricColumns,\n parseDescribeTableExtendedJson,\n} from \"./describe\";\nimport type {\n DescribeFetcher,\n MetricColumnMetadata,\n MetricConfigResolution,\n MetricSchema,\n MetricSyncFailure,\n MetricSyncResult,\n ResolvedMetricEntry,\n} from \"./types\";\n\n/**\n * Build the degraded schema emitted when an entry's columns are not\n * available — same key/source/lane as a real schema, with empty\n * measure/dimension allowlists and `degraded: true`.\n */\nexport function emptyMetricSchema(\n entry: Pick<MetricSchema, \"key\" | \"source\" | \"lane\">,\n): MetricSchema {\n return {\n key: entry.key,\n source: entry.source,\n lane: entry.lane,\n measures: [],\n dimensions: [],\n degraded: true,\n };\n}\n\n// Maximum number of in-flight DESCRIBE statements per syncMetrics pass.\nconst MV_DESCRIBE_CONCURRENCY = 10;\n\n// Outcome of describing a single metric entry.\ninterface MetricDescribeOutcome {\n index: number;\n schema: MetricSchema;\n failure?: MetricSyncFailure;\n}\n\n/**\n * Run schema synchronization for every entry in `definitions.json`.\n */\nexport async function syncMetrics(\n resolution: MetricConfigResolution,\n fetcher: DescribeFetcher,\n): Promise<MetricSyncResult> {\n const { entries } = resolution;\n const schemas = new Array<MetricSchema>(entries.length);\n const failureSlots = new Array<MetricSyncFailure | undefined>(entries.length);\n\n const failedOutcome = (\n index: number,\n entry: ResolvedMetricEntry,\n reason: string,\n transient: boolean,\n ): MetricDescribeOutcome => ({\n index,\n schema: emptyMetricSchema(entry),\n failure: { key: entry.key, source: entry.source, reason, transient },\n });\n\n const describeOne = async (\n entry: ResolvedMetricEntry,\n index: number,\n ): Promise<MetricDescribeOutcome> => {\n let response: DatabricksStatementExecutionResponse;\n try {\n response = await fetcher(entry.source);\n } catch (err) {\n const reason = `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(err)}`;\n // The DESCRIBE never ran (fetcher threw). Degrade by default (connectivity,\n // auth/permission, SDK/config) so the has-types gate can reuse committed\n // types; only the deny-list of deterministic client errors (bad warehouse\n // id 404, malformed request 400) surfaces as fatal — the same split the\n // query path and preflight make. (Truncated/multi-chunk and zero-column\n // responses are ran-and-failed, handled below and kept non-transient.)\n return failedOutcome(\n index,\n entry,\n reason,\n classifyBlockingFailure(err) !== \"deterministic\",\n );\n }\n\n const state = response.status?.state;\n if (state !== \"SUCCEEDED\" && state !== \"FAILED\") {\n return { index, schema: emptyMetricSchema(entry) };\n }\n\n let columns: MetricColumnMetadata[];\n try {\n const parsed = parseDescribeTableExtendedJson(response);\n columns = extractMetricColumns(parsed);\n } catch (err) {\n const reason = `Failed to extract columns from DESCRIBE response: ${(err as Error).message}`;\n return failedOutcome(index, entry, reason, false);\n }\n\n if (columns.length === 0) {\n const reason =\n \"DESCRIBE response yielded zero columns — check the response shape (top-level `columns` array or `schema.fields`).\";\n return failedOutcome(index, entry, reason, false);\n }\n\n const measures = columns.filter((c) => c.isMeasure);\n const dimensions = columns.filter((c) => !c.isMeasure);\n\n return {\n index,\n schema: {\n key: entry.key,\n source: entry.source,\n lane: entry.lane,\n measures,\n dimensions,\n },\n };\n };\n\n for (\n let offset = 0;\n offset < entries.length;\n offset += MV_DESCRIBE_CONCURRENCY\n ) {\n const slice = entries.slice(offset, offset + MV_DESCRIBE_CONCURRENCY);\n const settled = await Promise.allSettled(\n slice.map((entry, i) => describeOne(entry, offset + i)),\n );\n\n for (let i = 0; i < settled.length; i++) {\n const result = settled[i];\n if (result.status === \"fulfilled\") {\n const { index, schema, failure } = result.value;\n schemas[index] = schema;\n if (failure) {\n failureSlots[index] = failure;\n }\n } else {\n const index = offset + i;\n const entry = entries[index];\n const { schema, failure } = failedOutcome(\n index,\n entry,\n `DESCRIBE TABLE EXTENDED failed: ${getErrorDiagnostic(result.reason)}`,\n classifyBlockingFailure(result.reason) !== \"deterministic\",\n );\n schemas[index] = schema;\n failureSlots[index] = failure;\n }\n }\n }\n\n const failures = failureSlots.filter(\n (failure): failure is MetricSyncFailure => failure !== undefined,\n );\n\n return { schemas, failures };\n}\n"],"mappings":";;;;;;;;;AAqBA,SAAgB,kBACd,OACc;AACd,QAAO;EACL,KAAK,MAAM;EACX,QAAQ,MAAM;EACd,MAAM,MAAM;EACZ,UAAU,EAAE;EACZ,YAAY,EAAE;EACd,UAAU;EACX;;AAIH,MAAM,0BAA0B;;;;AAYhC,eAAsB,YACpB,YACA,SAC2B;CAC3B,MAAM,EAAE,YAAY;CACpB,MAAM,UAAU,IAAI,MAAoB,QAAQ,OAAO;CACvD,MAAM,eAAe,IAAI,MAAqC,QAAQ,OAAO;CAE7E,MAAM,iBACJ,OACA,OACA,QACA,eAC2B;EAC3B;EACA,QAAQ,kBAAkB,MAAM;EAChC,SAAS;GAAE,KAAK,MAAM;GAAK,QAAQ,MAAM;GAAQ;GAAQ;GAAW;EACrE;CAED,MAAM,cAAc,OAClB,OACA,UACmC;EACnC,IAAI;AACJ,MAAI;AACF,cAAW,MAAM,QAAQ,MAAM,OAAO;WAC/B,KAAK;AAQZ,UAAO,cACL,OACA,OATa,mCAAmC,mBAAmB,IAAI,IAWvE,wBAAwB,IAAI,KAAK,gBAClC;;EAGH,MAAM,QAAQ,SAAS,QAAQ;AAC/B,MAAI,UAAU,eAAe,UAAU,SACrC,QAAO;GAAE;GAAO,QAAQ,kBAAkB,MAAM;GAAE;EAGpD,IAAI;AACJ,MAAI;AAEF,aAAU,qBADK,+BAA+B,SAAS,CACjB;WAC/B,KAAK;AAEZ,UAAO,cAAc,OAAO,OADb,qDAAsD,IAAc,WACxC,MAAM;;AAGnD,MAAI,QAAQ,WAAW,EAGrB,QAAO,cAAc,OAAO,OAD1B,qHACyC,MAAM;EAGnD,MAAM,WAAW,QAAQ,QAAQ,MAAM,EAAE,UAAU;EACnD,MAAM,aAAa,QAAQ,QAAQ,MAAM,CAAC,EAAE,UAAU;AAEtD,SAAO;GACL;GACA,QAAQ;IACN,KAAK,MAAM;IACX,QAAQ,MAAM;IACd,MAAM,MAAM;IACZ;IACA;IACD;GACF;;AAGH,MACE,IAAI,SAAS,GACb,SAAS,QAAQ,QACjB,UAAU,yBACV;EACA,MAAM,QAAQ,QAAQ,MAAM,QAAQ,SAAS,wBAAwB;EACrE,MAAM,UAAU,MAAM,QAAQ,WAC5B,MAAM,KAAK,OAAO,MAAM,YAAY,OAAO,SAAS,EAAE,CAAC,CACxD;AAED,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;GACvC,MAAM,SAAS,QAAQ;AACvB,OAAI,OAAO,WAAW,aAAa;IACjC,MAAM,EAAE,OAAO,QAAQ,YAAY,OAAO;AAC1C,YAAQ,SAAS;AACjB,QAAI,QACF,cAAa,SAAS;UAEnB;IACL,MAAM,QAAQ,SAAS;IACvB,MAAM,QAAQ,QAAQ;IACtB,MAAM,EAAE,QAAQ,YAAY,cAC1B,OACA,OACA,mCAAmC,mBAAmB,OAAO,OAAO,IACpE,wBAAwB,OAAO,OAAO,KAAK,gBAC5C;AACD,YAAQ,SAAS;AACjB,iBAAa,SAAS;;;;AAS5B,QAAO;EAAE;EAAS,UAJD,aAAa,QAC3B,YAA0C,YAAY,OACxD;EAE2B"}
|
|
@@ -81,8 +81,11 @@ interface MetricSyncFailure {
|
|
|
81
81
|
/** Single human-readable reason (DESCRIBE failed, parse failed, zero columns). */
|
|
82
82
|
reason: string;
|
|
83
83
|
/**
|
|
84
|
-
* Whether the failure
|
|
85
|
-
*
|
|
84
|
+
* Whether the failure should degrade rather than fail the build. True for any
|
|
85
|
+
* DESCRIBE that never ran (connectivity, auth/permission, SDK/config) — the
|
|
86
|
+
* has-types gate reuses committed types. False for the deny-list of
|
|
87
|
+
* deterministic client errors (bad warehouse id 404, malformed request 400)
|
|
88
|
+
* and ran-and-failed responses (unparseable payload, zero columns).
|
|
86
89
|
*/
|
|
87
90
|
transient: boolean;
|
|
88
91
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/type-generator/mv-registry/types.ts"],"mappings":";;;;;AAUA;;;;;AAyCA;KAzCY,UAAA;;;;UAyCK,oBAAA;EACf,IAAA;EACA,IAAA;EAqEe;EAnEf,SAAA;;EAEA,WAAA;EAmEA;;;;;EA7DA,WAAA;
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/type-generator/mv-registry/types.ts"],"mappings":";;;;;AAUA;;;;;AAyCA;KAzCY,UAAA;;;;UAyCK,oBAAA;EACf,IAAA;EACA,IAAA;EAqEe;EAnEf,SAAA;;EAEA,WAAA;EAmEA;;;;;EA7DA,WAAA;EAgFe;;;;EA3Ef,MAAA;EA4ES;;;;;EAtET,UAAA;AAAA;;;;;;;UASe,YAAA;;EAEf,GAAA;;EAEA,MAAA;;EAEA,IAAA,EAAM,UAAA;;EAEN,QAAA,EAAU,oBAAA;;EAEV,UAAA,EAAY,oBAAA;;;;;;;EAOZ,QAAA;AAAA;;;;;;KAcU,eAAA,IACV,GAAA,aACG,OAAA,CAAQ,oCAAA;;;;;UAMI,iBAAA;;EAEf,GAAA;;EAEA,MAAA;;EAEA,MAAA;;;;;;;;EAQA,SAAA;AAAA;;;;;UAOe,gBAAA;EACf,OAAA,EAAS,YAAA;EACT,QAAA,EAAU,iBAAA;AAAA"}
|