@databricks/appkit 0.51.0 → 0.53.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 +3 -0
- package/dist/agents/databricks.d.ts +2 -3
- package/dist/agents/databricks.d.ts.map +1 -1
- package/dist/agents/databricks.js +5 -4
- package/dist/agents/databricks.js.map +1 -1
- package/dist/agents/supervisor-api.d.ts.map +1 -1
- package/dist/agents/supervisor-api.js +3 -1
- package/dist/agents/supervisor-api.js.map +1 -1
- package/dist/appkit/package.js +1 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +4 -2
- package/dist/cache/index.js.map +1 -1
- package/dist/connectors/files/client.js +2 -1
- package/dist/connectors/files/client.js.map +1 -1
- package/dist/connectors/genie/client.d.ts +2 -4
- package/dist/connectors/genie/client.js +2 -3
- package/dist/connectors/genie/client.js.map +1 -1
- package/dist/connectors/jobs/client.d.ts +2 -2
- package/dist/connectors/jobs/client.js +2 -1
- package/dist/connectors/jobs/client.js.map +1 -1
- package/dist/connectors/serving/client.d.ts +1 -1
- package/dist/connectors/serving/client.js +2 -1
- package/dist/connectors/serving/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/client.js +2 -1
- package/dist/connectors/sql-warehouse/client.js.map +1 -1
- package/dist/connectors/sql-warehouse/defaults.js.map +1 -1
- package/dist/connectors/sql-warehouse/warehouse-status-emitter.js.map +1 -1
- package/dist/connectors/vector-search/client.js.map +1 -1
- package/dist/context/client-options.js.map +1 -1
- package/dist/context/execution-context.d.ts +1 -1
- package/dist/context/service-context.d.ts +2 -1
- package/dist/context/service-context.d.ts.map +1 -1
- package/dist/context/service-context.js +8 -5
- package/dist/context/service-context.js.map +1 -1
- package/dist/core/appkit.d.ts +2 -1
- package/dist/core/appkit.d.ts.map +1 -1
- package/dist/core/appkit.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +4 -1
- package/dist/internal-telemetry/reporter.js.map +1 -1
- package/dist/plugins/analytics/analytics.d.ts.map +1 -1
- package/dist/plugins/analytics/analytics.js.map +1 -1
- package/dist/plugins/analytics/query.js.map +1 -1
- package/dist/plugins/analytics/result-delivery.js.map +1 -1
- package/dist/plugins/files/plugin.js +4 -3
- package/dist/plugins/files/plugin.js.map +1 -1
- package/dist/plugins/files/types.d.ts +2 -1
- package/dist/plugins/files/types.d.ts.map +1 -1
- package/dist/plugins/jobs/plugin.js.map +1 -1
- package/dist/plugins/jobs/types.d.ts +2 -1
- package/dist/plugins/jobs/types.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.d.ts.map +1 -1
- package/dist/plugins/lakebase/lakebase.js +5 -4
- package/dist/plugins/lakebase/lakebase.js.map +1 -1
- package/dist/shared/src/schemas/manifest.d.ts +2 -2
- package/dist/stream/arrow-stream-processor.js.map +1 -1
- package/dist/type-generator/errors.js +59 -1
- package/dist/type-generator/errors.js.map +1 -1
- package/dist/type-generator/index.js +105 -22
- package/dist/type-generator/index.js.map +1 -1
- package/dist/type-generator/mv-registry/describe.js.map +1 -1
- package/dist/type-generator/query-registry.js +60 -21
- package/dist/type-generator/query-registry.js.map +1 -1
- package/dist/type-generator/serving/fetcher.js +2 -1
- package/dist/type-generator/serving/fetcher.js.map +1 -1
- package/dist/type-generator/serving/generator.js +3 -2
- package/dist/type-generator/serving/generator.js.map +1 -1
- package/dist/type-generator/statement-result.js.map +1 -1
- package/dist/type-generator/types.js.map +1 -1
- package/dist/type-generator/vite-plugin.d.ts.map +1 -1
- package/dist/type-generator/vite-plugin.js +3 -2
- package/dist/type-generator/vite-plugin.js.map +1 -1
- package/dist/type-generator/warehouse-status.js.map +1 -1
- package/dist/workspace-client/client.js +57 -0
- package/dist/workspace-client/client.js.map +1 -0
- package/dist/workspace-client/errors.d.ts +2 -0
- package/dist/workspace-client/errors.js +3 -0
- package/dist/workspace-client/factory.d.ts +19 -0
- package/dist/workspace-client/factory.d.ts.map +1 -0
- package/dist/workspace-client/factory.js +24 -0
- package/dist/workspace-client/factory.js.map +1 -0
- package/dist/workspace-client/index.d.ts +4 -0
- package/dist/workspace-client/index.js +5 -0
- package/dist/workspace-client/legacy.d.ts +29 -0
- package/dist/workspace-client/legacy.d.ts.map +1 -0
- package/dist/workspace-client/legacy.js +23 -0
- package/dist/workspace-client/legacy.js.map +1 -0
- package/dist/workspace-client/types.d.ts +49 -0
- package/dist/workspace-client/types.d.ts.map +1 -0
- package/docs/api/appkit/Class.DatabricksAdapter.md +2 -3
- package/docs/api/appkit/Function.createApp.md +9 -9
- package/docs/api/appkit/Function.createWorkspaceClient.md +28 -0
- package/docs/api/appkit/Interface.WorkspaceClient.md +119 -0
- package/docs/api/appkit/Interface.WorkspaceClientOptions.md +47 -0
- package/docs/api/appkit.md +3 -0
- package/docs/development/type-generation.md +17 -2
- package/llms.txt +3 -0
- package/package.json +1 -1
- package/sbom.cdx.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","names":["manifest"],"sources":["../../../src/plugins/jobs/plugin.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport type { jobs as jobsTypes } from \"@databricks/sdk-experimental\";\nimport type express from \"express\";\nimport type {\n IAppRequest,\n IAppRouter,\n PluginExecutionSettings,\n StreamExecutionSettings,\n} from \"shared\";\nimport { toJSONSchema } from \"zod\";\nimport { JobsConnector } from \"../../connectors/jobs\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { ExecutionError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { ExecutionResult } from \"../../plugin\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport {\n JOBS_READ_DEFAULTS,\n JOBS_STREAM_DEFAULTS,\n JOBS_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { mapParams } from \"./params\";\nimport type {\n IJobsConfig,\n JobAPI,\n JobConfig,\n JobHandle,\n JobRunStatus,\n JobsExport,\n} from \"./types\";\n\nconst logger = createLogger(\"jobs\");\n\nconst DEFAULT_WAIT_TIMEOUT = 600_000;\nconst DEFAULT_POLL_INTERVAL = 5_000;\n/** Cap on param-key count when a job has no Zod schema. Jobs that need more keys must define a schema. */\nconst MAX_UNVALIDATED_PARAM_KEYS = 50;\n\n/** Replace upstream error messages with generic descriptions keyed by HTTP status. */\nfunction errorResult(status: number): ExecutionResult<never> {\n return {\n ok: false,\n status,\n message: STATUS_CODES[status] ?? \"Request failed\",\n };\n}\n\nfunction isTerminalRunState(state: string | undefined): boolean {\n return (\n state === \"TERMINATED\" || state === \"SKIPPED\" || state === \"INTERNAL_ERROR\"\n );\n}\n\n/** Exponential backoff (1.5x) with +/- 20% jitter, capped at `max`. */\nfunction nextPollDelay(\n current: number,\n max: number,\n): { delay: number; next: number } {\n const jitter = 1 + (Math.random() * 0.4 - 0.2);\n return {\n delay: Math.min(current * jitter, max),\n next: Math.min(current * 1.5, max),\n };\n}\n\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve) => {\n if (signal?.aborted) {\n resolve();\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nclass JobsPlugin extends Plugin {\n static manifest = manifest as PluginManifest;\n\n protected declare config: IJobsConfig;\n private connector: JobsConnector;\n private jobIds: Record<string, number> = {};\n private jobConfigs: Record<string, JobConfig> = {};\n private jobKeys: string[] = [];\n\n /**\n * Scans process.env for DATABRICKS_JOB_* keys and merges with explicit config.\n * Explicit config wins for per-job overrides; auto-discovered jobs get default `{}` config.\n */\n static discoverJobs(config: IJobsConfig): Record<string, JobConfig> {\n const explicit = config.jobs ?? {};\n const discovered: Record<string, JobConfig> = {};\n\n const prefix = \"DATABRICKS_JOB_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n if (key === \"DATABRICKS_JOB_ID\") continue;\n const suffix = key.slice(prefix.length);\n if (!suffix || !process.env[key]) continue;\n const jobKey = suffix.toLowerCase();\n if (!(jobKey in explicit)) {\n discovered[jobKey] = {};\n }\n }\n\n // Single-job shorthand: DATABRICKS_JOB_ID maps to \"default\" key\n if (\n process.env.DATABRICKS_JOB_ID &&\n Object.keys(explicit).length === 0 &&\n Object.keys(discovered).length === 0\n ) {\n discovered.default = {};\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured jobs.\n * Each job key maps to a `DATABRICKS_JOB_{KEY_UPPERCASE}` env var (or `DATABRICKS_JOB_ID` for \"default\").\n */\n static getResourceRequirements(config: IJobsConfig): ResourceRequirement[] {\n const jobs = JobsPlugin.discoverJobs(config);\n return Object.keys(jobs).map((key) => ({\n type: ResourceType.JOB,\n alias: `job-${key}`,\n resourceKey: `job-${key}`,\n description: `Databricks Job \"${key}\"`,\n permission: \"CAN_MANAGE_RUN\" as const,\n fields: {\n id: {\n env:\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`,\n description: `Job ID for \"${key}\"`,\n },\n },\n required: true,\n }));\n }\n\n constructor(config: IJobsConfig) {\n super(config);\n this.config = config;\n this.connector = new JobsConnector({\n telemetry: config.telemetry,\n });\n\n const jobs = JobsPlugin.discoverJobs(config);\n this.jobKeys = Object.keys(jobs);\n this.jobConfigs = jobs;\n\n for (const key of this.jobKeys) {\n const envVar =\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`;\n const jobIdStr = process.env[envVar];\n if (jobIdStr) {\n const parsed = Number.parseInt(jobIdStr, 10);\n if (!Number.isNaN(parsed)) {\n this.jobIds[key] = parsed;\n }\n }\n }\n }\n\n async setup() {\n logger.info(\n `Jobs plugin initialized with ${this.jobKeys.length} job(s): ${this.jobKeys.join(\", \")}`,\n );\n }\n\n private get client() {\n return getWorkspaceClient();\n }\n\n private getJobId(jobKey: string): number {\n const id = this.jobIds[jobKey];\n if (!id) {\n const envVar =\n jobKey === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${jobKey.toUpperCase()}`;\n throw new Error(\n `Job \"${jobKey}\" has no configured job ID. Set ${envVar} env var.`,\n );\n }\n return id;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_READ_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n cache: { ...JOBS_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n private _writeSettings(): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_WRITE_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n },\n };\n }\n\n /**\n * Validates params against the job's Zod schema (if any) and maps them\n * to SDK request fields based on the task type. Shared by runNow and runAndWait.\n */\n private _validateAndMap(\n jobKey: string,\n params?: Record<string, unknown>,\n ): Record<string, unknown> {\n const jobConfig = this.jobConfigs[jobKey];\n let validated = params;\n\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(params ?? {});\n if (!result.success) {\n throw new ValidationError(\n `Parameter validation failed for job \"${jobKey}\": ${result.error.message}`,\n );\n }\n validated = result.data as Record<string, unknown>;\n }\n\n return jobConfig?.taskType && validated\n ? mapParams(jobConfig.taskType, validated)\n : (validated ?? {});\n }\n\n /**\n * Creates a JobAPI for a specific configured job key.\n * Each method is scoped to the job's configured ID.\n */\n protected createJobAPI(jobKey: string): JobAPI {\n const jobId = this.getJobId(jobKey);\n const jobConfig = this.jobConfigs[jobKey];\n // Capture `this` for use in the async generator\n const self = this;\n // Eagerly capture the client and userId so that when createJobAPI is\n // called inside an asUser() proxy (which runs in user context), the\n // closures below use the user-scoped client instead of falling back\n // to the service principal when the ALS context has already exited.\n const client = this.client;\n const userKey = getCurrentUserId();\n\n /**\n * Verify that `runId` belongs to this job's configured `jobId`. Returns\n * null if the run is in scope; otherwise returns a 404 `ExecutionResult`.\n * Prevents cross-job access via the `/:jobKey/runs/:runId` HTTP surface.\n */\n const verifyRunScope = async (\n runId: number,\n ): Promise<ExecutionResult<never> | null> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return null;\n };\n\n return {\n runNow: async (\n params?: Record<string, unknown>,\n ): Promise<ExecutionResult<jobsTypes.RunNowResponse>> => {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const result = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n async *runAndWait(\n params?: Record<string, unknown>,\n signal?: AbortSignal,\n ): AsyncGenerator<JobRunStatus, void, unknown> {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const runResult = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n\n if (!runResult.ok) {\n throw new ExecutionError(\"Failed to trigger job run\");\n }\n const runId = runResult.data.run_id;\n if (!runId) {\n throw new Error(\"runNow did not return a run_id\");\n }\n\n const basePollInterval =\n self.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;\n const maxPollInterval = basePollInterval * 6;\n const timeout = jobConfig?.waitTimeout ?? DEFAULT_WAIT_TIMEOUT;\n const startTime = Date.now();\n let currentInterval = basePollInterval;\n\n while (!signal?.aborted) {\n if (Date.now() - startTime > timeout) {\n throw new Error(\n `Job run ${runId} polling timeout after ${timeout}ms`,\n );\n }\n\n const runStatusResult = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n {\n default: {\n ...JOBS_READ_DEFAULTS,\n cache: { enabled: false },\n },\n },\n userKey,\n );\n if (!runStatusResult.ok) {\n throw new ExecutionError(\n `Failed to poll run status for run ${runId}`,\n );\n }\n const run = runStatusResult.data;\n const state = run.state?.life_cycle_state;\n\n yield { status: state, timestamp: Date.now(), run };\n\n if (isTerminalRunState(state)) return;\n\n const { delay, next } = nextPollDelay(\n currentInterval,\n maxPollInterval,\n );\n currentInterval = next;\n await abortableSleep(delay, signal);\n }\n },\n\n lastRun: async (): Promise<\n ExecutionResult<jobsTypes.BaseRun | undefined>\n > => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: 1 },\n signal,\n ),\n self._readSettings([\"jobs:lastRun\", jobKey]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n return { ok: true, data: result.data[0] };\n },\n\n listRuns: async (options?: {\n limit?: number;\n }): Promise<ExecutionResult<jobsTypes.BaseRun[]>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: options?.limit },\n signal,\n ),\n self._readSettings([\n \"jobs:listRuns\",\n jobKey,\n options?.limit ?? \"default\",\n ]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getRun: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.Run>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return result;\n },\n\n getRunOutput: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.RunOutput>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.getRunOutput(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRunOutput\", jobKey, runId]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n cancelRun: async (runId: number): Promise<ExecutionResult<void>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.cancelRun(client, { run_id: runId }, signal),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getJob: async (): Promise<ExecutionResult<jobsTypes.Job>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getJob(client, { job_id: jobId }, signal),\n self._readSettings([\"jobs:getJob\", jobKey]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n };\n }\n\n /**\n * Resolve `:jobKey` from the request. Returns the key and ID,\n * or sends a 404 and returns `{ jobKey: undefined, jobId: undefined }`.\n */\n private _resolveJob(\n req: express.Request,\n res: express.Response,\n ):\n | { jobKey: string; jobId: number }\n | { jobKey: undefined; jobId: undefined } {\n const jobKey = req.params.jobKey;\n if (!this.jobKeys.includes(jobKey)) {\n const safeKey = jobKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown job \"${safeKey}\"`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n const jobId = this.jobIds[jobKey];\n if (!jobId) {\n res.status(404).json({\n error: `Job \"${jobKey}\" has no configured job ID`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n return { jobKey, jobId };\n }\n\n private _sendStatusError(res: express.Response, status: number): void {\n res.status(status).json({\n error: STATUS_CODES[status] ?? \"Unknown Error\",\n plugin: this.name,\n });\n }\n\n /**\n * Validate params from an HTTP request body. Eager validation lets streaming\n * requests get a clean 400 instead of a generic SSE error event. Throws\n * ValidationError so handlers can map to a 400 response via their catch block.\n */\n private _parseRunParams(\n jobKey: string,\n rawParams: unknown,\n ): Record<string, unknown> | undefined {\n if (\n rawParams !== undefined &&\n (typeof rawParams !== \"object\" ||\n rawParams === null ||\n Array.isArray(rawParams))\n ) {\n throw new ValidationError(\"params must be a plain object\");\n }\n\n const jobConfig = this.jobConfigs[jobKey];\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(rawParams ?? {});\n if (!result.success) {\n throw new ValidationError(\"Invalid job parameters\");\n }\n // Pass rawParams — not result.data — to avoid double-transforming\n // when _validateAndMap calls safeParse again downstream.\n return rawParams as Record<string, unknown>;\n }\n // No schema. Either reject (no taskType) or enforce a key cap so that\n // untrusted clients can't spread arbitrarily many fields into the SDK.\n if (rawParams !== undefined) {\n if (!jobConfig?.taskType) {\n throw new ValidationError(\"This job does not accept parameters\");\n }\n const keyCount = Object.keys(rawParams as Record<string, unknown>).length;\n if (keyCount > MAX_UNVALIDATED_PARAM_KEYS) {\n throw new ValidationError(\n `Too many parameters (${keyCount}). Define a Zod schema to accept more than ${MAX_UNVALIDATED_PARAM_KEYS}.`,\n );\n }\n }\n return rawParams as Record<string, unknown> | undefined;\n }\n\n private async _handleRun(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const stream = req.query.stream === \"true\";\n\n try {\n const params = this._parseRunParams(jobKey, req.body?.params);\n const api = this.createJobAPI(jobKey);\n\n if (stream) {\n const streamSettings: StreamExecutionSettings = {\n default: JOBS_STREAM_DEFAULTS,\n };\n await this.executeStream<JobRunStatus>(\n res,\n (signal) => api.runAndWait(params, signal),\n streamSettings,\n );\n } else {\n const result = await api.runNow(params);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runId: result.data.run_id });\n }\n } catch (error) {\n if (error instanceof ValidationError) {\n if (!res.headersSent) {\n res.status(400).json({ error: error.message, plugin: this.name });\n }\n return;\n }\n logger.error(\"Run failed for job %s: %O\", jobKey, error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Run failed\", plugin: this.name });\n }\n }\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"run\",\n method: \"post\",\n path: \"/:jobKey/run\",\n handler: (req, res) => this._handleRun(req, res),\n });\n\n // GET /:jobKey/runs\n this.route(router, {\n name: \"runs\",\n method: \"get\",\n path: \"/:jobKey/runs\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const limit = Math.max(\n 1,\n Math.min(Number.parseInt(req.query.limit as string, 10) || 20, 100),\n );\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.listRuns({ limit });\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runs: result.data });\n } catch (error) {\n logger.error(\"List runs failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"List runs failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/runs/:runId\n this.route(router, {\n name: \"run-detail\",\n method: \"get\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.getRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\n } catch (error) {\n logger.error(\n \"Get run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res.status(500).json({ error: \"Get run failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/status\n this.route(router, {\n name: \"status\",\n method: \"get\",\n path: \"/:jobKey/status\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.lastRun();\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({\n status: result.data?.state?.life_cycle_state ?? null,\n run: result.data ?? null,\n });\n } catch (error) {\n logger.error(\"Status check failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"Status check failed\", plugin: this.name });\n }\n },\n });\n\n // DELETE /:jobKey/runs/:runId\n this.route(router, {\n name: \"cancel-run\",\n method: \"delete\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.cancelRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.status(204).end();\n } catch (error) {\n logger.error(\n \"Cancel run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res\n .status(500)\n .json({ error: \"Cancel run failed\", plugin: this.name });\n }\n },\n });\n }\n\n exports(): JobsExport {\n const resolveJob = (jobKey: string): JobHandle => {\n if (!this.jobKeys.includes(jobKey)) {\n throw new Error(\n `Unknown job \"${jobKey}\". Available jobs: ${this.jobKeys.join(\", \")}`,\n );\n }\n\n const spApi = this.createJobAPI(jobKey);\n\n return {\n ...spApi,\n asUser: (req: IAppRequest) => {\n const userPlugin = this.asUser(req) as JobsPlugin;\n return userPlugin.createJobAPI(jobKey);\n },\n };\n };\n\n return resolveJob as JobsExport;\n }\n\n clientConfig(): Record<string, unknown> {\n const jobs: Record<string, { params: unknown; taskType: string | null }> =\n {};\n for (const key of this.jobKeys) {\n const config = this.jobConfigs[key];\n jobs[key] = {\n params: config?.params ? toJSONSchema(config.params) : null,\n taskType: config?.taskType ?? null,\n };\n }\n return { jobs };\n }\n}\n\n/**\n * @internal\n */\nexport const jobs = toPlugin(JobsPlugin);\n\n/**\n * @internal\n */\nexport { JobsPlugin };\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"plugin.js","names":["manifest"],"sources":["../../../src/plugins/jobs/plugin.ts"],"sourcesContent":["import { STATUS_CODES } from \"node:http\";\nimport type express from \"express\";\nimport type {\n IAppRequest,\n IAppRouter,\n PluginExecutionSettings,\n StreamExecutionSettings,\n} from \"shared\";\nimport { toJSONSchema } from \"zod\";\nimport { JobsConnector } from \"../../connectors/jobs\";\nimport { getCurrentUserId, getWorkspaceClient } from \"../../context\";\nimport { ExecutionError, ValidationError } from \"../../errors\";\nimport { createLogger } from \"../../logging/logger\";\nimport type { ExecutionResult } from \"../../plugin\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest, ResourceRequirement } from \"../../registry\";\nimport { ResourceType } from \"../../registry\";\nimport type { jobs as jobsTypes } from \"../../workspace-client\";\nimport {\n JOBS_READ_DEFAULTS,\n JOBS_STREAM_DEFAULTS,\n JOBS_WRITE_DEFAULTS,\n} from \"./defaults\";\nimport manifest from \"./manifest.json\";\nimport { mapParams } from \"./params\";\nimport type {\n IJobsConfig,\n JobAPI,\n JobConfig,\n JobHandle,\n JobRunStatus,\n JobsExport,\n} from \"./types\";\n\nconst logger = createLogger(\"jobs\");\n\nconst DEFAULT_WAIT_TIMEOUT = 600_000;\nconst DEFAULT_POLL_INTERVAL = 5_000;\n/** Cap on param-key count when a job has no Zod schema. Jobs that need more keys must define a schema. */\nconst MAX_UNVALIDATED_PARAM_KEYS = 50;\n\n/** Replace upstream error messages with generic descriptions keyed by HTTP status. */\nfunction errorResult(status: number): ExecutionResult<never> {\n return {\n ok: false,\n status,\n message: STATUS_CODES[status] ?? \"Request failed\",\n };\n}\n\nfunction isTerminalRunState(state: string | undefined): boolean {\n return (\n state === \"TERMINATED\" || state === \"SKIPPED\" || state === \"INTERNAL_ERROR\"\n );\n}\n\n/** Exponential backoff (1.5x) with +/- 20% jitter, capped at `max`. */\nfunction nextPollDelay(\n current: number,\n max: number,\n): { delay: number; next: number } {\n const jitter = 1 + (Math.random() * 0.4 - 0.2);\n return {\n delay: Math.min(current * jitter, max),\n next: Math.min(current * 1.5, max),\n };\n}\n\nfunction abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise<void>((resolve) => {\n if (signal?.aborted) {\n resolve();\n return;\n }\n const timer = setTimeout(resolve, ms);\n signal?.addEventListener(\n \"abort\",\n () => {\n clearTimeout(timer);\n resolve();\n },\n { once: true },\n );\n });\n}\n\nclass JobsPlugin extends Plugin {\n static manifest = manifest as PluginManifest;\n\n protected declare config: IJobsConfig;\n private connector: JobsConnector;\n private jobIds: Record<string, number> = {};\n private jobConfigs: Record<string, JobConfig> = {};\n private jobKeys: string[] = [];\n\n /**\n * Scans process.env for DATABRICKS_JOB_* keys and merges with explicit config.\n * Explicit config wins for per-job overrides; auto-discovered jobs get default `{}` config.\n */\n static discoverJobs(config: IJobsConfig): Record<string, JobConfig> {\n const explicit = config.jobs ?? {};\n const discovered: Record<string, JobConfig> = {};\n\n const prefix = \"DATABRICKS_JOB_\";\n for (const key of Object.keys(process.env)) {\n if (!key.startsWith(prefix)) continue;\n if (key === \"DATABRICKS_JOB_ID\") continue;\n const suffix = key.slice(prefix.length);\n if (!suffix || !process.env[key]) continue;\n const jobKey = suffix.toLowerCase();\n if (!(jobKey in explicit)) {\n discovered[jobKey] = {};\n }\n }\n\n // Single-job shorthand: DATABRICKS_JOB_ID maps to \"default\" key\n if (\n process.env.DATABRICKS_JOB_ID &&\n Object.keys(explicit).length === 0 &&\n Object.keys(discovered).length === 0\n ) {\n discovered.default = {};\n }\n\n return { ...discovered, ...explicit };\n }\n\n /**\n * Generates resource requirements dynamically from discovered + configured jobs.\n * Each job key maps to a `DATABRICKS_JOB_{KEY_UPPERCASE}` env var (or `DATABRICKS_JOB_ID` for \"default\").\n */\n static getResourceRequirements(config: IJobsConfig): ResourceRequirement[] {\n const jobs = JobsPlugin.discoverJobs(config);\n return Object.keys(jobs).map((key) => ({\n type: ResourceType.JOB,\n alias: `job-${key}`,\n resourceKey: `job-${key}`,\n description: `Databricks Job \"${key}\"`,\n permission: \"CAN_MANAGE_RUN\" as const,\n fields: {\n id: {\n env:\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`,\n description: `Job ID for \"${key}\"`,\n },\n },\n required: true,\n }));\n }\n\n constructor(config: IJobsConfig) {\n super(config);\n this.config = config;\n this.connector = new JobsConnector({\n telemetry: config.telemetry,\n });\n\n const jobs = JobsPlugin.discoverJobs(config);\n this.jobKeys = Object.keys(jobs);\n this.jobConfigs = jobs;\n\n for (const key of this.jobKeys) {\n const envVar =\n key === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${key.toUpperCase()}`;\n const jobIdStr = process.env[envVar];\n if (jobIdStr) {\n const parsed = Number.parseInt(jobIdStr, 10);\n if (!Number.isNaN(parsed)) {\n this.jobIds[key] = parsed;\n }\n }\n }\n }\n\n async setup() {\n logger.info(\n `Jobs plugin initialized with ${this.jobKeys.length} job(s): ${this.jobKeys.join(\", \")}`,\n );\n }\n\n private get client() {\n return getWorkspaceClient();\n }\n\n private getJobId(jobKey: string): number {\n const id = this.jobIds[jobKey];\n if (!id) {\n const envVar =\n jobKey === \"default\"\n ? \"DATABRICKS_JOB_ID\"\n : `DATABRICKS_JOB_${jobKey.toUpperCase()}`;\n throw new Error(\n `Job \"${jobKey}\" has no configured job ID. Set ${envVar} env var.`,\n );\n }\n return id;\n }\n\n private _readSettings(\n cacheKey: (string | number | object)[],\n ): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_READ_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n cache: { ...JOBS_READ_DEFAULTS.cache, cacheKey },\n },\n };\n }\n\n private _writeSettings(): PluginExecutionSettings {\n return {\n default: {\n ...JOBS_WRITE_DEFAULTS,\n ...(this.config.timeout != null && { timeout: this.config.timeout }),\n },\n };\n }\n\n /**\n * Validates params against the job's Zod schema (if any) and maps them\n * to SDK request fields based on the task type. Shared by runNow and runAndWait.\n */\n private _validateAndMap(\n jobKey: string,\n params?: Record<string, unknown>,\n ): Record<string, unknown> {\n const jobConfig = this.jobConfigs[jobKey];\n let validated = params;\n\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(params ?? {});\n if (!result.success) {\n throw new ValidationError(\n `Parameter validation failed for job \"${jobKey}\": ${result.error.message}`,\n );\n }\n validated = result.data as Record<string, unknown>;\n }\n\n return jobConfig?.taskType && validated\n ? mapParams(jobConfig.taskType, validated)\n : (validated ?? {});\n }\n\n /**\n * Creates a JobAPI for a specific configured job key.\n * Each method is scoped to the job's configured ID.\n */\n protected createJobAPI(jobKey: string): JobAPI {\n const jobId = this.getJobId(jobKey);\n const jobConfig = this.jobConfigs[jobKey];\n // Capture `this` for use in the async generator\n const self = this;\n // Eagerly capture the client and userId so that when createJobAPI is\n // called inside an asUser() proxy (which runs in user context), the\n // closures below use the user-scoped client instead of falling back\n // to the service principal when the ALS context has already exited.\n const client = this.client;\n const userKey = getCurrentUserId();\n\n /**\n * Verify that `runId` belongs to this job's configured `jobId`. Returns\n * null if the run is in scope; otherwise returns a 404 `ExecutionResult`.\n * Prevents cross-job access via the `/:jobKey/runs/:runId` HTTP surface.\n */\n const verifyRunScope = async (\n runId: number,\n ): Promise<ExecutionResult<never> | null> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return null;\n };\n\n return {\n runNow: async (\n params?: Record<string, unknown>,\n ): Promise<ExecutionResult<jobsTypes.RunNowResponse>> => {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const result = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n async *runAndWait(\n params?: Record<string, unknown>,\n signal?: AbortSignal,\n ): AsyncGenerator<JobRunStatus, void, unknown> {\n const sdkFields = self._validateAndMap(jobKey, params);\n\n const runResult = await self.execute(\n async (signal) =>\n self.connector.runNow(\n client,\n { ...sdkFields, job_id: jobId },\n signal,\n ),\n self._writeSettings(),\n userKey,\n );\n\n if (!runResult.ok) {\n throw new ExecutionError(\"Failed to trigger job run\");\n }\n const runId = runResult.data.run_id;\n if (!runId) {\n throw new Error(\"runNow did not return a run_id\");\n }\n\n const basePollInterval =\n self.config.pollIntervalMs ?? DEFAULT_POLL_INTERVAL;\n const maxPollInterval = basePollInterval * 6;\n const timeout = jobConfig?.waitTimeout ?? DEFAULT_WAIT_TIMEOUT;\n const startTime = Date.now();\n let currentInterval = basePollInterval;\n\n while (!signal?.aborted) {\n if (Date.now() - startTime > timeout) {\n throw new Error(\n `Job run ${runId} polling timeout after ${timeout}ms`,\n );\n }\n\n const runStatusResult = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n {\n default: {\n ...JOBS_READ_DEFAULTS,\n cache: { enabled: false },\n },\n },\n userKey,\n );\n if (!runStatusResult.ok) {\n throw new ExecutionError(\n `Failed to poll run status for run ${runId}`,\n );\n }\n const run = runStatusResult.data;\n const state = run.state?.life_cycle_state;\n\n yield { status: state, timestamp: Date.now(), run };\n\n if (isTerminalRunState(state)) return;\n\n const { delay, next } = nextPollDelay(\n currentInterval,\n maxPollInterval,\n );\n currentInterval = next;\n await abortableSleep(delay, signal);\n }\n },\n\n lastRun: async (): Promise<\n ExecutionResult<jobsTypes.BaseRun | undefined>\n > => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: 1 },\n signal,\n ),\n self._readSettings([\"jobs:lastRun\", jobKey]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n return { ok: true, data: result.data[0] };\n },\n\n listRuns: async (options?: {\n limit?: number;\n }): Promise<ExecutionResult<jobsTypes.BaseRun[]>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.listRuns(\n client,\n { job_id: jobId, limit: options?.limit },\n signal,\n ),\n self._readSettings([\n \"jobs:listRuns\",\n jobKey,\n options?.limit ?? \"default\",\n ]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getRun: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.Run>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getRun(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRun\", jobKey, runId]),\n userKey,\n );\n if (!result.ok) return errorResult(result.status);\n if (result.data.job_id !== jobId) return errorResult(404);\n return result;\n },\n\n getRunOutput: async (\n runId: number,\n ): Promise<ExecutionResult<jobsTypes.RunOutput>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.getRunOutput(client, { run_id: runId }, signal),\n self._readSettings([\"jobs:getRunOutput\", jobKey, runId]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n cancelRun: async (runId: number): Promise<ExecutionResult<void>> => {\n const scopeError = await verifyRunScope(runId);\n if (scopeError) return scopeError;\n const result = await self.execute(\n async (signal) =>\n self.connector.cancelRun(client, { run_id: runId }, signal),\n self._writeSettings(),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n\n getJob: async (): Promise<ExecutionResult<jobsTypes.Job>> => {\n const result = await self.execute(\n async (signal) =>\n self.connector.getJob(client, { job_id: jobId }, signal),\n self._readSettings([\"jobs:getJob\", jobKey]),\n userKey,\n );\n return result.ok ? result : errorResult(result.status);\n },\n };\n }\n\n /**\n * Resolve `:jobKey` from the request. Returns the key and ID,\n * or sends a 404 and returns `{ jobKey: undefined, jobId: undefined }`.\n */\n private _resolveJob(\n req: express.Request,\n res: express.Response,\n ):\n | { jobKey: string; jobId: number }\n | { jobKey: undefined; jobId: undefined } {\n const jobKey = req.params.jobKey;\n if (!this.jobKeys.includes(jobKey)) {\n const safeKey = jobKey.replace(/[^a-zA-Z0-9_-]/g, \"\");\n res.status(404).json({\n error: `Unknown job \"${safeKey}\"`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n const jobId = this.jobIds[jobKey];\n if (!jobId) {\n res.status(404).json({\n error: `Job \"${jobKey}\" has no configured job ID`,\n plugin: this.name,\n });\n return { jobKey: undefined, jobId: undefined };\n }\n return { jobKey, jobId };\n }\n\n private _sendStatusError(res: express.Response, status: number): void {\n res.status(status).json({\n error: STATUS_CODES[status] ?? \"Unknown Error\",\n plugin: this.name,\n });\n }\n\n /**\n * Validate params from an HTTP request body. Eager validation lets streaming\n * requests get a clean 400 instead of a generic SSE error event. Throws\n * ValidationError so handlers can map to a 400 response via their catch block.\n */\n private _parseRunParams(\n jobKey: string,\n rawParams: unknown,\n ): Record<string, unknown> | undefined {\n if (\n rawParams !== undefined &&\n (typeof rawParams !== \"object\" ||\n rawParams === null ||\n Array.isArray(rawParams))\n ) {\n throw new ValidationError(\"params must be a plain object\");\n }\n\n const jobConfig = this.jobConfigs[jobKey];\n if (jobConfig?.params) {\n const result = jobConfig.params.safeParse(rawParams ?? {});\n if (!result.success) {\n throw new ValidationError(\"Invalid job parameters\");\n }\n // Pass rawParams — not result.data — to avoid double-transforming\n // when _validateAndMap calls safeParse again downstream.\n return rawParams as Record<string, unknown>;\n }\n // No schema. Either reject (no taskType) or enforce a key cap so that\n // untrusted clients can't spread arbitrarily many fields into the SDK.\n if (rawParams !== undefined) {\n if (!jobConfig?.taskType) {\n throw new ValidationError(\"This job does not accept parameters\");\n }\n const keyCount = Object.keys(rawParams as Record<string, unknown>).length;\n if (keyCount > MAX_UNVALIDATED_PARAM_KEYS) {\n throw new ValidationError(\n `Too many parameters (${keyCount}). Define a Zod schema to accept more than ${MAX_UNVALIDATED_PARAM_KEYS}.`,\n );\n }\n }\n return rawParams as Record<string, unknown> | undefined;\n }\n\n private async _handleRun(\n req: express.Request,\n res: express.Response,\n ): Promise<void> {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const stream = req.query.stream === \"true\";\n\n try {\n const params = this._parseRunParams(jobKey, req.body?.params);\n const api = this.createJobAPI(jobKey);\n\n if (stream) {\n const streamSettings: StreamExecutionSettings = {\n default: JOBS_STREAM_DEFAULTS,\n };\n await this.executeStream<JobRunStatus>(\n res,\n (signal) => api.runAndWait(params, signal),\n streamSettings,\n );\n } else {\n const result = await api.runNow(params);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runId: result.data.run_id });\n }\n } catch (error) {\n if (error instanceof ValidationError) {\n if (!res.headersSent) {\n res.status(400).json({ error: error.message, plugin: this.name });\n }\n return;\n }\n logger.error(\"Run failed for job %s: %O\", jobKey, error);\n if (!res.headersSent) {\n res.status(500).json({ error: \"Run failed\", plugin: this.name });\n }\n }\n }\n\n injectRoutes(router: IAppRouter) {\n this.route(router, {\n name: \"run\",\n method: \"post\",\n path: \"/:jobKey/run\",\n handler: (req, res) => this._handleRun(req, res),\n });\n\n // GET /:jobKey/runs\n this.route(router, {\n name: \"runs\",\n method: \"get\",\n path: \"/:jobKey/runs\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const limit = Math.max(\n 1,\n Math.min(Number.parseInt(req.query.limit as string, 10) || 20, 100),\n );\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.listRuns({ limit });\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({ runs: result.data });\n } catch (error) {\n logger.error(\"List runs failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"List runs failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/runs/:runId\n this.route(router, {\n name: \"run-detail\",\n method: \"get\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.getRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json(result.data);\n } catch (error) {\n logger.error(\n \"Get run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res.status(500).json({ error: \"Get run failed\", plugin: this.name });\n }\n },\n });\n\n // GET /:jobKey/status\n this.route(router, {\n name: \"status\",\n method: \"get\",\n path: \"/:jobKey/status\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.lastRun();\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.json({\n status: result.data?.state?.life_cycle_state ?? null,\n run: result.data ?? null,\n });\n } catch (error) {\n logger.error(\"Status check failed for job %s: %O\", jobKey, error);\n res\n .status(500)\n .json({ error: \"Status check failed\", plugin: this.name });\n }\n },\n });\n\n // DELETE /:jobKey/runs/:runId\n this.route(router, {\n name: \"cancel-run\",\n method: \"delete\",\n path: \"/:jobKey/runs/:runId\",\n handler: async (req: express.Request, res: express.Response) => {\n const { jobKey } = this._resolveJob(req, res);\n if (!jobKey) return;\n\n const runId = Number.parseInt(req.params.runId, 10);\n if (Number.isNaN(runId) || runId <= 0) {\n res.status(400).json({ error: \"Invalid runId\", plugin: this.name });\n return;\n }\n\n try {\n const api = this.createJobAPI(jobKey);\n const result = await api.cancelRun(runId);\n if (!result.ok) {\n this._sendStatusError(res, result.status);\n return;\n }\n res.status(204).end();\n } catch (error) {\n logger.error(\n \"Cancel run failed for job %s run %d: %O\",\n jobKey,\n runId,\n error,\n );\n res\n .status(500)\n .json({ error: \"Cancel run failed\", plugin: this.name });\n }\n },\n });\n }\n\n exports(): JobsExport {\n const resolveJob = (jobKey: string): JobHandle => {\n if (!this.jobKeys.includes(jobKey)) {\n throw new Error(\n `Unknown job \"${jobKey}\". Available jobs: ${this.jobKeys.join(\", \")}`,\n );\n }\n\n const spApi = this.createJobAPI(jobKey);\n\n return {\n ...spApi,\n asUser: (req: IAppRequest) => {\n const userPlugin = this.asUser(req) as JobsPlugin;\n return userPlugin.createJobAPI(jobKey);\n },\n };\n };\n\n return resolveJob as JobsExport;\n }\n\n clientConfig(): Record<string, unknown> {\n const jobs: Record<string, { params: unknown; taskType: string | null }> =\n {};\n for (const key of this.jobKeys) {\n const config = this.jobConfigs[key];\n jobs[key] = {\n params: config?.params ? toJSONSchema(config.params) : null,\n taskType: config?.taskType ?? null,\n };\n }\n return { jobs };\n }\n}\n\n/**\n * @internal\n */\nexport const jobs = toPlugin(JobsPlugin);\n\n/**\n * @internal\n */\nexport { JobsPlugin };\n"],"mappings":"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"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { BasePluginConfig, IAppRequest } from "../../shared/src/plugin.js";
|
|
2
2
|
import "../../shared/src/index.js";
|
|
3
|
+
import { jobs } from "../../workspace-client/types.js";
|
|
4
|
+
import "../../workspace-client/index.js";
|
|
3
5
|
import { ExecutionResult } from "../../plugin/execution-result.js";
|
|
4
6
|
import "../../plugin/index.js";
|
|
5
7
|
import { z } from "zod";
|
|
6
|
-
import { jobs } from "@databricks/sdk-experimental";
|
|
7
8
|
|
|
8
9
|
//#region src/plugins/jobs/types.d.ts
|
|
9
10
|
/** Supported task types for job parameter mapping. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/jobs/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../../../src/plugins/jobs/types.ts"],"mappings":";;;;;;;;;;KAMY,QAAA;;UASK,SAAA;;EAEf,WAAA;EAXkB;EAalB,QAAA,GAAW,QAAA;EAbO;EAelB,MAAA,GAAS,CAAA,CAAE,OAAA,CAAQ,MAAA;AAAA;;UAIJ,YAAA;EACf,MAAA;EACA,SAAA;EACA,GAAA,EAAK,IAAA,CAAK,GAAA;AAAA;;UAIK,MAAA;EAbf;EAeA,MAAA,CACE,MAAA,GAAS,MAAA,oBACR,OAAA,CAAQ,eAAA,CAAgB,IAAA,CAAK,cAAA;EAfhC;EAiBA,UAAA,CACE,MAAA,GAAS,MAAA,mBACT,MAAA,GAAS,WAAA,GACR,cAAA,CAAe,YAAA;EApBP;EAsBX,OAAA,IAAW,OAAA,CAAQ,eAAA,CAAgB,IAAA,CAAK,OAAA;EAtBf;EAwBzB,QAAA,CAAS,OAAA;IACP,KAAA;EAAA,IACE,OAAA,CAAQ,eAAA,CAAgB,IAAA,CAAK,OAAA;;EAEjC,MAAA,CAAO,KAAA,WAAgB,OAAA,CAAQ,eAAA,CAAgB,IAAA,CAAK,GAAA;EAvBpD;EAyBA,YAAA,CAAa,KAAA,WAAgB,OAAA,CAAQ,eAAA,CAAgB,IAAA,CAAK,SAAA;EAvB1D;EAyBA,SAAA,CAAU,KAAA,WAAgB,OAAA,CAAQ,eAAA;EAzBxB;EA2BV,MAAA,IAAU,OAAA,CAAQ,eAAA,CAAgB,IAAA,CAAK,GAAA;AAAA;AAvBzC;AAAA,UA2BiB,WAAA,SAAoB,gBAAA;;EAEnC,OAAA;EAzB2B;EA2B3B,cAAA;EA3BG;EA6BH,IAAA,GAAO,MAAA,SAAe,SAAA;AAAA;;;;;KAOZ,SAAA,GAAY,MAAA;EACtB,MAAA,GAAS,GAAA,EAAK,WAAA,KAAgB,MAAA;AAAA;;;;;;;;;;;;;;;;;;;KAqBpB,UAAA,IAAc,MAAA,aAAmB,SAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lakebase.d.ts","names":[],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA;;;;;;;;;;;;;;;;cAAa,cAAA,SAAuB,MAAA,YAAkB,YAAA;
|
|
1
|
+
{"version":3,"file":"lakebase.d.ts","names":[],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiEA;;;;;;;;;;;;;;;;cAAa,cAAA,SAAuB,MAAA,YAAkB,YAAA;EAkOU;EAAA,OAhOvD,QAAA,EAAuB,cAAA;EAAA,UAEZ,MAAA,EAAQ,eAAA;EAAA,QAClB,IAAA;EAAA,QACA,cAAA;EAwEL;;;;;;;;EA9DG,KAAA,CAAA,GAAK,OAAA;EAhBqD;;;;;;;;;;;;;;;;;;EA2E1D,KAAA,WAAgB,cAAA,OAAA,CACpB,IAAA,UACA,MAAA,eACC,OAAA,CAAQ,WAAA,CAAY,CAAA;EAAA;;;;;;;;;;;;;;EAAA,QAmBT,oBAAA;EA4HZ;;;;;;;;;;;;EA7FI,QAAA,CAAA,GAAY,OAAA;EArDI;;;;;EAAA,QAkFd,KAAA;cAEI,MAAA,EAAQ,eAAA;EAAA,QAYZ,cAAA;EA2CR,aAAA,CAAA,GAAiB,mBAAA;EAIX,gBAAA,CACJ,IAAA,UACA,IAAA,WACA,MAAA,GAAS,WAAA,GACR,OAAA;EAIH,OAAA,CAAQ,IAAA,GAJE,cAAA,GAIoD,MAAA,SAAA,YAAA;;;;;UAQtD,gBAAA;EA3PA;;;;;;;;;;;;EAoRR,OAAA,CAAA;UAGwB,YAAA;sBA3LF,cAAA,QAAc,IAAA,UACtB,MAAA,iBAEX,OAAA,CAAQ,WAAA,CAAY,CAAA;;;;;;;;;;iBA/Ff,QAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAkSG,QAAA,EAAQ,QAAA,QAAA,cAAA,EAAA,eAAA"}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createLogger } from "../../logging/logger.js";
|
|
2
2
|
import { createLakebasePoolManager } from "../../connectors/lakebase/pool-manager.js";
|
|
3
|
+
import { createWorkspaceClient } from "../../workspace-client/factory.js";
|
|
4
|
+
import "../../workspace-client/index.js";
|
|
3
5
|
import { getClientOptions } from "../../context/client-options.js";
|
|
4
6
|
import { getUserContext } from "../../context/execution-context.js";
|
|
5
7
|
import { RoutingPool } from "../../connectors/lakebase/routing-pool.js";
|
|
@@ -12,7 +14,6 @@ import { defineTool, executeFromRegistry, toolsFromRegistry } from "../../core/a
|
|
|
12
14
|
import { assertReadOnlySql } from "../../core/agent/tools/sql-policy.js";
|
|
13
15
|
import manifest_default from "./manifest.js";
|
|
14
16
|
import { z } from "zod";
|
|
15
|
-
import { WorkspaceClient } from "@databricks/sdk-experimental";
|
|
16
17
|
|
|
17
18
|
//#region src/plugins/lakebase/lakebase.ts
|
|
18
19
|
const logger = createLogger("lakebase");
|
|
@@ -65,7 +66,7 @@ var LakebasePlugin = class extends Plugin {
|
|
|
65
66
|
async setup() {
|
|
66
67
|
const poolConfig = {
|
|
67
68
|
...this.config.pool,
|
|
68
|
-
workspaceClient: this.config.pool?.workspaceClient ??
|
|
69
|
+
workspaceClient: this.config.pool?.workspaceClient ?? createWorkspaceClient({ clientOptions: getClientOptions() }).toLegacyWorkspaceClient()
|
|
69
70
|
};
|
|
70
71
|
const user = await getUsernameWithApiLookup(poolConfig);
|
|
71
72
|
const spPool = createLakebasePool({
|
|
@@ -84,7 +85,7 @@ var LakebasePlugin = class extends Plugin {
|
|
|
84
85
|
const userKey = ctx.userEmail ?? ctx.userId;
|
|
85
86
|
const isNew = !oboManager.hasPool(userKey);
|
|
86
87
|
const pool = oboManager.getPool(userKey, {
|
|
87
|
-
workspaceClient: ctx.client,
|
|
88
|
+
workspaceClient: ctx.client.toLegacyWorkspaceClient(),
|
|
88
89
|
user: userKey
|
|
89
90
|
}, ctx.tokenFingerprint);
|
|
90
91
|
if (isNew) logger.debug("Created OBO pool for user (total: %d)", oboManager.size);
|
|
@@ -227,7 +228,7 @@ var LakebasePlugin = class extends Plugin {
|
|
|
227
228
|
const user = ctx.userEmail ?? ctx.userId;
|
|
228
229
|
return {
|
|
229
230
|
...this.config.pool,
|
|
230
|
-
workspaceClient: ctx.client,
|
|
231
|
+
workspaceClient: ctx.client.toLegacyWorkspaceClient(),
|
|
231
232
|
user
|
|
232
233
|
};
|
|
233
234
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lakebase.js","names":["manifest"],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"sourcesContent":["import { WorkspaceClient } from \"@databricks/sdk-experimental\";\nimport type { QueryResult, QueryResultRow } from \"pg\";\nimport type { AgentToolDefinition, ToolProvider } from \"shared\";\nimport { z } from \"zod\";\nimport {\n createLakebasePool,\n createLakebasePoolManager,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n type LakebasePool,\n type LakebasePoolManager,\n RoutingPool,\n} from \"../../connectors/lakebase\";\nimport { getClientOptions } from \"../../context/client-options\";\nimport { getUserContext } from \"../../context/execution-context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport manifest from \"./manifest.json\";\nimport type { ILakebaseConfig } from \"./types\";\n\nconst logger = createLogger(\"lakebase\");\n\n/** Default pool settings for per-user OBO pools. */\nconst OBO_POOL_DEFAULTS = {\n max: 3,\n allowExitOnIdle: true,\n idleTimeoutMillis: 30_000,\n};\n\n/**\n * AppKit plugin for Databricks Lakebase Autoscaling.\n *\n * Wraps `@databricks/lakebase` to provide a standard `pg.Pool` with automatic\n * OAuth token refresh, integrated with AppKit's logger and OpenTelemetry setup.\n *\n * Supports On-Behalf-Of (OBO) via `asUser(req)` — each user gets a separate\n * `pg.Pool` authenticated with their Databricks identity, enabling features\n * like Row-Level Security (RLS). Routing is handled transparently by\n * {@link RoutingPool}, which reads the execution context set by the base\n * class `asUser()`.\n *\n * @example\n * ```ts\n * import { createApp, lakebase, server } from \"@databricks/appkit\";\n *\n * const AppKit = await createApp({\n * plugins: [server(), lakebase()],\n * });\n *\n * // Service principal query\n * const result = await AppKit.lakebase.query(\"SELECT * FROM users WHERE id = $1\", [userId]);\n *\n * // User-scoped query (per-user pool, RLS enforced)\n * const mine = await AppKit.lakebase.asUser(req).query(\"SELECT * FROM my_data\");\n * ```\n */\nexport class LakebasePlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"lakebase\">;\n\n protected declare config: ILakebaseConfig;\n private pool: RoutingPool | null = null;\n private oboPoolManager: LakebasePoolManager | null = null;\n\n /**\n * Initializes the Lakebase connection pool and OBO pool manager.\n * Called automatically by AppKit during the plugin setup phase.\n *\n * Creates a {@link RoutingPool} that automatically routes queries to either\n * the service-principal pool or a per-user pool based on the execution\n * context (set by `Plugin.asUser(req)` via AsyncLocalStorage).\n */\n async setup() {\n const poolConfig = {\n ...this.config.pool,\n workspaceClient:\n this.config.pool?.workspaceClient ??\n new WorkspaceClient({}, getClientOptions()),\n };\n const user = await getUsernameWithApiLookup(poolConfig);\n\n const spPool = createLakebasePool({ ...poolConfig, user });\n logger.info(\"Lakebase SP pool initialized\");\n\n this.oboPoolManager = createLakebasePoolManager({\n ...poolConfig,\n ...OBO_POOL_DEFAULTS,\n });\n logger.info(\"Lakebase OBO pool manager initialized\");\n\n const oboManager = this.oboPoolManager;\n this.pool = new RoutingPool(spPool, (ctx) => {\n if (!oboManager) throw new Error(\"OBO pool manager not initialized\");\n // Lakebase OAuth roles use email as the postgres role when available\n const userKey = ctx.userEmail ?? ctx.userId;\n const isNew = !oboManager.hasPool(userKey);\n const pool = oboManager.getPool(\n userKey,\n { workspaceClient: ctx.client, user: userKey },\n ctx.tokenFingerprint,\n );\n if (isNew) {\n logger.debug(\"Created OBO pool for user (total: %d)\", oboManager.size);\n }\n return pool;\n });\n }\n\n /**\n * Executes a parameterized SQL query against the Lakebase pool.\n *\n * When called inside `asUser(req)`, the query automatically routes to\n * the per-user pool via {@link RoutingPool}.\n *\n * @param text - SQL query string, using `$1`, `$2`, ... placeholders\n * @param values - Parameter values corresponding to placeholders\n * @returns Query result with typed rows\n *\n * @example\n * ```ts\n * const result = await AppKit.lakebase.query<{ id: number; name: string }>(\n * \"SELECT id, name FROM users WHERE active = $1\",\n * [true],\n * );\n * ```\n */\n async query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n return this.pool!.query<T>(text, values);\n }\n\n /**\n * Execute a single statement inside a `BEGIN READ ONLY … ROLLBACK`\n * transaction on a dedicated client.\n *\n * The three commands MUST share a connection — a naive\n * `pool.query(\"BEGIN READ ONLY; <stmt>; ROLLBACK\")` batch cannot accept\n * parameter values (PostgreSQL's Extended Query protocol rejects multi-\n * statement prepared queries), which would silently break every\n * parameterized query the agent tool issues.\n *\n * Returns the raw `rows` array for the user's statement. Side effects the\n * statement may attempt (writes, writable-function side effects) are\n * rejected by PostgreSQL under the read-only transaction posture.\n */\n private async runReadOnlyStatement(\n text: string,\n values?: unknown[],\n ): Promise<unknown[]> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup()\n const client = await this.pool!.connect();\n try {\n await client.query(\"BEGIN READ ONLY\");\n const result = await client.query(text, values);\n return result.rows;\n } finally {\n try {\n await client.query(\"ROLLBACK\");\n } finally {\n client.release();\n }\n }\n }\n\n /**\n * Gracefully drains and closes all connection pools (SP + OBO).\n *\n * Runs as the plugin's `shutdown()` hook (phase 3 of the core lifecycle\n * manager's graceful shutdown), NOT in `abortActiveOperations()` (phase 2):\n * other plugins' `shutdown()` hooks may still need database connections to\n * drain state,\n * so the pools must outlive the abort phase. `pg.Pool#end()` waits for\n * checked-out clients to be released, so hooks running concurrently with\n * this one can still finish their in-flight queries. Errors are caught\n * and logged; this hook never throws.\n */\n async shutdown(): Promise<void> {\n if (this.pool) {\n logger.info(\"Closing Lakebase SP pool\");\n try {\n await this.pool.end();\n } catch (err) {\n logger.error(\"Error closing Lakebase SP pool: %O\", err);\n }\n this.pool = null;\n }\n if (this.oboPoolManager) {\n logger.info(\n \"Closing all Lakebase OBO pools (%d)\",\n this.oboPoolManager.size,\n );\n try {\n await this.oboPoolManager.closeAll();\n } catch (err) {\n logger.error(\"Error closing Lakebase OBO pools: %O\", err);\n }\n this.oboPoolManager = null;\n }\n }\n\n /**\n * Agent tool registry. Empty by default — the Lakebase plugin does NOT\n * expose its SQL connection to LLM agents unless the developer explicitly\n * opts in via `config.exposeAsAgentTool`. See {@link buildQueryTool}.\n */\n private tools: Record<string, ReturnType<typeof this.buildQueryTool>> = {};\n\n constructor(config: ILakebaseConfig) {\n super(config);\n this.config = config;\n if (config.exposeAsAgentTool) {\n this.tools = { query: this.buildQueryTool(config.exposeAsAgentTool) };\n logger.warn(\n \"Lakebase agent tool is enabled (readOnly=%s). Every agent with access to this plugin can execute SQL against the Lakebase database as the requesting user's identity.\",\n config.exposeAsAgentTool.readOnly !== false,\n );\n }\n }\n\n private buildQueryTool(\n opt: NonNullable<ILakebaseConfig[\"exposeAsAgentTool\"]>,\n ) {\n const readOnly = opt.readOnly !== false;\n return defineTool({\n description: readOnly\n ? \"Execute a read-only SQL query against the Lakebase PostgreSQL database. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted. Use $1, $2, etc. as placeholders and pass values separately.\"\n : \"Execute a parameterized SQL statement against the Lakebase PostgreSQL database. Use $1, $2, etc. as placeholders and pass values separately. This tool can modify data; every invocation requires explicit human approval.\",\n schema: z.object({\n text: z\n .string()\n .describe(\n \"SQL statement with $1, $2, ... placeholders for parameters\",\n ),\n values: z\n .array(z.unknown())\n .optional()\n .describe(\"Parameter values corresponding to placeholders\"),\n }),\n annotations: {\n effect: readOnly ? \"read\" : \"destructive\",\n idempotent: false,\n requiresUserContext: true,\n },\n execute: async (args, signal) => {\n // Matches the files plugin pattern: the pg connection API\n // doesn't accept AbortSignal in its current shape, so deeper\n // mid-call cancellation needs a separate plumbing pass on the\n // connector. This entry check still catches the common case —\n // a tool dispatched after the user already cancelled the\n // stream — and unwinds cleanly instead of running to\n // completion against the SQL warehouse.\n signal?.throwIfAborted();\n if (readOnly) {\n assertReadOnlySql(args.text);\n return this.runReadOnlyStatement(args.text, args.values);\n }\n const result = await this.query(args.text, args.values);\n return result.rows;\n },\n });\n }\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the pool config for the current execution context.\n * Inside `asUser(req)`, returns user-scoped config; otherwise SP config.\n */\n private activePoolConfig() {\n const ctx = getUserContext();\n if (ctx) {\n const user = ctx.userEmail ?? ctx.userId;\n return { ...this.config.pool, workspaceClient: ctx.client, user };\n }\n return this.config.pool;\n }\n\n /**\n * Returns the plugin's public API, accessible via `AppKit.lakebase`.\n *\n * - `pool` — The connection pool (routes to per-user pool when inside `asUser(req)`)\n * - `query` — Convenience method for executing parameterized SQL queries\n * - `getOrmConfig()` — Returns a config object compatible with Drizzle, TypeORM, Sequelize, etc.\n * Inside `asUser(req)`, returns user-scoped config.\n * - `getPgConfig()` — Returns a `pg.PoolConfig` object for manual pool construction.\n * Inside `asUser(req)`, returns user-scoped config.\n *\n * Use `AppKit.lakebase.asUser(req)` to get the same API backed by a per-user pool.\n */\n exports() {\n return {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n pool: this.pool! as LakebasePool,\n query: this.query.bind(this),\n getOrmConfig: () => getLakebaseOrmConfig(this.activePoolConfig()),\n getPgConfig: () => getLakebasePgConfig(this.activePoolConfig()),\n };\n }\n}\n\n/**\n * @internal\n */\nexport const lakebase = toPlugin(LakebasePlugin);\n"],"mappings":";;;;;;;;;;;;;;;;;AA6BA,MAAM,SAAS,aAAa,WAAW;;AAGvC,MAAM,oBAAoB;CACxB,KAAK;CACL,iBAAiB;CACjB,mBAAmB;CACpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BD,IAAa,iBAAb,cAAoC,OAA+B;;CAEjE,OAAO,WAAWA;CAGlB,AAAQ,OAA2B;CACnC,AAAQ,iBAA6C;;;;;;;;;CAUrD,MAAM,QAAQ;EACZ,MAAM,aAAa;GACjB,GAAG,KAAK,OAAO;GACf,iBACE,KAAK,OAAO,MAAM,mBAClB,IAAI,gBAAgB,EAAE,EAAE,kBAAkB,CAAC;GAC9C;EACD,MAAM,OAAO,MAAM,yBAAyB,WAAW;EAEvD,MAAM,SAAS,mBAAmB;GAAE,GAAG;GAAY;GAAM,CAAC;AAC1D,SAAO,KAAK,+BAA+B;AAE3C,OAAK,iBAAiB,0BAA0B;GAC9C,GAAG;GACH,GAAG;GACJ,CAAC;AACF,SAAO,KAAK,wCAAwC;EAEpD,MAAM,aAAa,KAAK;AACxB,OAAK,OAAO,IAAI,YAAY,SAAS,QAAQ;AAC3C,OAAI,CAAC,WAAY,OAAM,IAAI,MAAM,mCAAmC;GAEpE,MAAM,UAAU,IAAI,aAAa,IAAI;GACrC,MAAM,QAAQ,CAAC,WAAW,QAAQ,QAAQ;GAC1C,MAAM,OAAO,WAAW,QACtB,SACA;IAAE,iBAAiB,IAAI;IAAQ,MAAM;IAAS,EAC9C,IAAI,iBACL;AACD,OAAI,MACF,QAAO,MAAM,yCAAyC,WAAW,KAAK;AAExE,UAAO;IACP;;;;;;;;;;;;;;;;;;;;CAqBJ,MAAM,MACJ,MACA,QACyB;AAEzB,SAAO,KAAK,KAAM,MAAS,MAAM,OAAO;;;;;;;;;;;;;;;;CAiB1C,MAAc,qBACZ,MACA,QACoB;EAEpB,MAAM,SAAS,MAAM,KAAK,KAAM,SAAS;AACzC,MAAI;AACF,SAAM,OAAO,MAAM,kBAAkB;AAErC,WADe,MAAM,OAAO,MAAM,MAAM,OAAO,EACjC;YACN;AACR,OAAI;AACF,UAAM,OAAO,MAAM,WAAW;aACtB;AACR,WAAO,SAAS;;;;;;;;;;;;;;;;CAiBtB,MAAM,WAA0B;AAC9B,MAAI,KAAK,MAAM;AACb,UAAO,KAAK,2BAA2B;AACvC,OAAI;AACF,UAAM,KAAK,KAAK,KAAK;YACd,KAAK;AACZ,WAAO,MAAM,sCAAsC,IAAI;;AAEzD,QAAK,OAAO;;AAEd,MAAI,KAAK,gBAAgB;AACvB,UAAO,KACL,uCACA,KAAK,eAAe,KACrB;AACD,OAAI;AACF,UAAM,KAAK,eAAe,UAAU;YAC7B,KAAK;AACZ,WAAO,MAAM,wCAAwC,IAAI;;AAE3D,QAAK,iBAAiB;;;;;;;;CAS1B,AAAQ,QAAgE,EAAE;CAE1E,YAAY,QAAyB;AACnC,QAAM,OAAO;AACb,OAAK,SAAS;AACd,MAAI,OAAO,mBAAmB;AAC5B,QAAK,QAAQ,EAAE,OAAO,KAAK,eAAe,OAAO,kBAAkB,EAAE;AACrE,UAAO,KACL,yKACA,OAAO,kBAAkB,aAAa,MACvC;;;CAIL,AAAQ,eACN,KACA;EACA,MAAM,WAAW,IAAI,aAAa;AAClC,SAAO,WAAW;GAChB,aAAa,WACT,iNACA;GACJ,QAAQ,EAAE,OAAO;IACf,MAAM,EACH,QAAQ,CACR,SACC,6DACD;IACH,QAAQ,EACL,MAAM,EAAE,SAAS,CAAC,CAClB,UAAU,CACV,SAAS,iDAAiD;IAC9D,CAAC;GACF,aAAa;IACX,QAAQ,WAAW,SAAS;IAC5B,YAAY;IACZ,qBAAqB;IACtB;GACD,SAAS,OAAO,MAAM,WAAW;AAQ/B,YAAQ,gBAAgB;AACxB,QAAI,UAAU;AACZ,uBAAkB,KAAK,KAAK;AAC5B,YAAO,KAAK,qBAAqB,KAAK,MAAM,KAAK,OAAO;;AAG1D,YADe,MAAM,KAAK,MAAM,KAAK,MAAM,KAAK,OAAO,EACzC;;GAEjB,CAAC;;CAGJ,gBAAuC;AACrC,SAAO,kBAAkB,KAAK,MAAM;;CAGtC,MAAM,iBACJ,MACA,MACA,QACkB;AAClB,SAAO,oBAAoB,KAAK,OAAO,MAAM,MAAM,OAAO;;CAG5D,QAAQ,MAAwD;AAC9D,SAAO,oBAAoB,KAAK,MAAM,KAAK,OAAO,KAAK;;;;;;CAOzD,AAAQ,mBAAmB;EACzB,MAAM,MAAM,gBAAgB;AAC5B,MAAI,KAAK;GACP,MAAM,OAAO,IAAI,aAAa,IAAI;AAClC,UAAO;IAAE,GAAG,KAAK,OAAO;IAAM,iBAAiB,IAAI;IAAQ;IAAM;;AAEnE,SAAO,KAAK,OAAO;;;;;;;;;;;;;;CAerB,UAAU;AACR,SAAO;GAEL,MAAM,KAAK;GACX,OAAO,KAAK,MAAM,KAAK,KAAK;GAC5B,oBAAoB,qBAAqB,KAAK,kBAAkB,CAAC;GACjE,mBAAmB,oBAAoB,KAAK,kBAAkB,CAAC;GAChE;;;;;;AAOL,MAAa,WAAW,SAAS,eAAe"}
|
|
1
|
+
{"version":3,"file":"lakebase.js","names":["manifest"],"sources":["../../../src/plugins/lakebase/lakebase.ts"],"sourcesContent":["import type { QueryResult, QueryResultRow } from \"pg\";\nimport type { AgentToolDefinition, ToolProvider } from \"shared\";\nimport { z } from \"zod\";\nimport {\n createLakebasePool,\n createLakebasePoolManager,\n getLakebaseOrmConfig,\n getLakebasePgConfig,\n getUsernameWithApiLookup,\n type LakebasePool,\n type LakebasePoolManager,\n RoutingPool,\n} from \"../../connectors/lakebase\";\nimport { getClientOptions } from \"../../context/client-options\";\nimport { getUserContext } from \"../../context/execution-context\";\nimport { buildToolkitEntries } from \"../../core/agent/build-toolkit\";\nimport {\n defineTool,\n executeFromRegistry,\n toolsFromRegistry,\n} from \"../../core/agent/tools/define-tool\";\nimport { assertReadOnlySql } from \"../../core/agent/tools/sql-policy\";\nimport { createLogger } from \"../../logging/logger\";\nimport { Plugin, toPlugin } from \"../../plugin\";\nimport type { PluginManifest } from \"../../registry\";\nimport { createWorkspaceClient } from \"../../workspace-client\";\nimport manifest from \"./manifest.json\";\nimport type { ILakebaseConfig } from \"./types\";\n\nconst logger = createLogger(\"lakebase\");\n\n/** Default pool settings for per-user OBO pools. */\nconst OBO_POOL_DEFAULTS = {\n max: 3,\n allowExitOnIdle: true,\n idleTimeoutMillis: 30_000,\n};\n\n/**\n * AppKit plugin for Databricks Lakebase Autoscaling.\n *\n * Wraps `@databricks/lakebase` to provide a standard `pg.Pool` with automatic\n * OAuth token refresh, integrated with AppKit's logger and OpenTelemetry setup.\n *\n * Supports On-Behalf-Of (OBO) via `asUser(req)` — each user gets a separate\n * `pg.Pool` authenticated with their Databricks identity, enabling features\n * like Row-Level Security (RLS). Routing is handled transparently by\n * {@link RoutingPool}, which reads the execution context set by the base\n * class `asUser()`.\n *\n * @example\n * ```ts\n * import { createApp, lakebase, server } from \"@databricks/appkit\";\n *\n * const AppKit = await createApp({\n * plugins: [server(), lakebase()],\n * });\n *\n * // Service principal query\n * const result = await AppKit.lakebase.query(\"SELECT * FROM users WHERE id = $1\", [userId]);\n *\n * // User-scoped query (per-user pool, RLS enforced)\n * const mine = await AppKit.lakebase.asUser(req).query(\"SELECT * FROM my_data\");\n * ```\n */\nexport class LakebasePlugin extends Plugin implements ToolProvider {\n /** Plugin manifest declaring metadata and resource requirements */\n static manifest = manifest as PluginManifest<\"lakebase\">;\n\n protected declare config: ILakebaseConfig;\n private pool: RoutingPool | null = null;\n private oboPoolManager: LakebasePoolManager | null = null;\n\n /**\n * Initializes the Lakebase connection pool and OBO pool manager.\n * Called automatically by AppKit during the plugin setup phase.\n *\n * Creates a {@link RoutingPool} that automatically routes queries to either\n * the service-principal pool or a per-user pool based on the execution\n * context (set by `Plugin.asUser(req)` via AsyncLocalStorage).\n */\n async setup() {\n const poolConfig = {\n ...this.config.pool,\n workspaceClient:\n this.config.pool?.workspaceClient ??\n createWorkspaceClient({\n clientOptions: getClientOptions(),\n }).toLegacyWorkspaceClient(),\n };\n const user = await getUsernameWithApiLookup(poolConfig);\n\n const spPool = createLakebasePool({ ...poolConfig, user });\n logger.info(\"Lakebase SP pool initialized\");\n\n this.oboPoolManager = createLakebasePoolManager({\n ...poolConfig,\n ...OBO_POOL_DEFAULTS,\n });\n logger.info(\"Lakebase OBO pool manager initialized\");\n\n const oboManager = this.oboPoolManager;\n this.pool = new RoutingPool(spPool, (ctx) => {\n if (!oboManager) throw new Error(\"OBO pool manager not initialized\");\n // Lakebase OAuth roles use email as the postgres role when available\n const userKey = ctx.userEmail ?? ctx.userId;\n const isNew = !oboManager.hasPool(userKey);\n const pool = oboManager.getPool(\n userKey,\n {\n workspaceClient: ctx.client.toLegacyWorkspaceClient(),\n user: userKey,\n },\n ctx.tokenFingerprint,\n );\n if (isNew) {\n logger.debug(\"Created OBO pool for user (total: %d)\", oboManager.size);\n }\n return pool;\n });\n }\n\n /**\n * Executes a parameterized SQL query against the Lakebase pool.\n *\n * When called inside `asUser(req)`, the query automatically routes to\n * the per-user pool via {@link RoutingPool}.\n *\n * @param text - SQL query string, using `$1`, `$2`, ... placeholders\n * @param values - Parameter values corresponding to placeholders\n * @returns Query result with typed rows\n *\n * @example\n * ```ts\n * const result = await AppKit.lakebase.query<{ id: number; name: string }>(\n * \"SELECT id, name FROM users WHERE active = $1\",\n * [true],\n * );\n * ```\n */\n async query<T extends QueryResultRow = any>(\n text: string,\n values?: unknown[],\n ): Promise<QueryResult<T>> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n return this.pool!.query<T>(text, values);\n }\n\n /**\n * Execute a single statement inside a `BEGIN READ ONLY … ROLLBACK`\n * transaction on a dedicated client.\n *\n * The three commands MUST share a connection — a naive\n * `pool.query(\"BEGIN READ ONLY; <stmt>; ROLLBACK\")` batch cannot accept\n * parameter values (PostgreSQL's Extended Query protocol rejects multi-\n * statement prepared queries), which would silently break every\n * parameterized query the agent tool issues.\n *\n * Returns the raw `rows` array for the user's statement. Side effects the\n * statement may attempt (writes, writable-function side effects) are\n * rejected by PostgreSQL under the read-only transaction posture.\n */\n private async runReadOnlyStatement(\n text: string,\n values?: unknown[],\n ): Promise<unknown[]> {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup()\n const client = await this.pool!.connect();\n try {\n await client.query(\"BEGIN READ ONLY\");\n const result = await client.query(text, values);\n return result.rows;\n } finally {\n try {\n await client.query(\"ROLLBACK\");\n } finally {\n client.release();\n }\n }\n }\n\n /**\n * Gracefully drains and closes all connection pools (SP + OBO).\n *\n * Runs as the plugin's `shutdown()` hook (phase 3 of the core lifecycle\n * manager's graceful shutdown), NOT in `abortActiveOperations()` (phase 2):\n * other plugins' `shutdown()` hooks may still need database connections to\n * drain state,\n * so the pools must outlive the abort phase. `pg.Pool#end()` waits for\n * checked-out clients to be released, so hooks running concurrently with\n * this one can still finish their in-flight queries. Errors are caught\n * and logged; this hook never throws.\n */\n async shutdown(): Promise<void> {\n if (this.pool) {\n logger.info(\"Closing Lakebase SP pool\");\n try {\n await this.pool.end();\n } catch (err) {\n logger.error(\"Error closing Lakebase SP pool: %O\", err);\n }\n this.pool = null;\n }\n if (this.oboPoolManager) {\n logger.info(\n \"Closing all Lakebase OBO pools (%d)\",\n this.oboPoolManager.size,\n );\n try {\n await this.oboPoolManager.closeAll();\n } catch (err) {\n logger.error(\"Error closing Lakebase OBO pools: %O\", err);\n }\n this.oboPoolManager = null;\n }\n }\n\n /**\n * Agent tool registry. Empty by default — the Lakebase plugin does NOT\n * expose its SQL connection to LLM agents unless the developer explicitly\n * opts in via `config.exposeAsAgentTool`. See {@link buildQueryTool}.\n */\n private tools: Record<string, ReturnType<typeof this.buildQueryTool>> = {};\n\n constructor(config: ILakebaseConfig) {\n super(config);\n this.config = config;\n if (config.exposeAsAgentTool) {\n this.tools = { query: this.buildQueryTool(config.exposeAsAgentTool) };\n logger.warn(\n \"Lakebase agent tool is enabled (readOnly=%s). Every agent with access to this plugin can execute SQL against the Lakebase database as the requesting user's identity.\",\n config.exposeAsAgentTool.readOnly !== false,\n );\n }\n }\n\n private buildQueryTool(\n opt: NonNullable<ILakebaseConfig[\"exposeAsAgentTool\"]>,\n ) {\n const readOnly = opt.readOnly !== false;\n return defineTool({\n description: readOnly\n ? \"Execute a read-only SQL query against the Lakebase PostgreSQL database. Only SELECT, WITH, SHOW, EXPLAIN, and DESCRIBE statements are accepted. Use $1, $2, etc. as placeholders and pass values separately.\"\n : \"Execute a parameterized SQL statement against the Lakebase PostgreSQL database. Use $1, $2, etc. as placeholders and pass values separately. This tool can modify data; every invocation requires explicit human approval.\",\n schema: z.object({\n text: z\n .string()\n .describe(\n \"SQL statement with $1, $2, ... placeholders for parameters\",\n ),\n values: z\n .array(z.unknown())\n .optional()\n .describe(\"Parameter values corresponding to placeholders\"),\n }),\n annotations: {\n effect: readOnly ? \"read\" : \"destructive\",\n idempotent: false,\n requiresUserContext: true,\n },\n execute: async (args, signal) => {\n // Matches the files plugin pattern: the pg connection API\n // doesn't accept AbortSignal in its current shape, so deeper\n // mid-call cancellation needs a separate plumbing pass on the\n // connector. This entry check still catches the common case —\n // a tool dispatched after the user already cancelled the\n // stream — and unwinds cleanly instead of running to\n // completion against the SQL warehouse.\n signal?.throwIfAborted();\n if (readOnly) {\n assertReadOnlySql(args.text);\n return this.runReadOnlyStatement(args.text, args.values);\n }\n const result = await this.query(args.text, args.values);\n return result.rows;\n },\n });\n }\n\n getAgentTools(): AgentToolDefinition[] {\n return toolsFromRegistry(this.tools);\n }\n\n async executeAgentTool(\n name: string,\n args: unknown,\n signal?: AbortSignal,\n ): Promise<unknown> {\n return executeFromRegistry(this.tools, name, args, signal);\n }\n\n toolkit(opts?: import(\"../../core/agent/types\").ToolkitOptions) {\n return buildToolkitEntries(this.name, this.tools, opts);\n }\n\n /**\n * Returns the pool config for the current execution context.\n * Inside `asUser(req)`, returns user-scoped config; otherwise SP config.\n */\n private activePoolConfig() {\n const ctx = getUserContext();\n if (ctx) {\n const user = ctx.userEmail ?? ctx.userId;\n return {\n ...this.config.pool,\n workspaceClient: ctx.client.toLegacyWorkspaceClient(),\n user,\n };\n }\n return this.config.pool;\n }\n\n /**\n * Returns the plugin's public API, accessible via `AppKit.lakebase`.\n *\n * - `pool` — The connection pool (routes to per-user pool when inside `asUser(req)`)\n * - `query` — Convenience method for executing parameterized SQL queries\n * - `getOrmConfig()` — Returns a config object compatible with Drizzle, TypeORM, Sequelize, etc.\n * Inside `asUser(req)`, returns user-scoped config.\n * - `getPgConfig()` — Returns a `pg.PoolConfig` object for manual pool construction.\n * Inside `asUser(req)`, returns user-scoped config.\n *\n * Use `AppKit.lakebase.asUser(req)` to get the same API backed by a per-user pool.\n */\n exports() {\n return {\n // biome-ignore lint/style/noNonNullAssertion: pool is guaranteed non-null after setup(), which AppKit always awaits before exposing the plugin API\n pool: this.pool! as LakebasePool,\n query: this.query.bind(this),\n getOrmConfig: () => getLakebaseOrmConfig(this.activePoolConfig()),\n getPgConfig: () => getLakebasePgConfig(this.activePoolConfig()),\n };\n }\n}\n\n/**\n * @internal\n */\nexport const lakebase = toPlugin(LakebasePlugin);\n"],"mappings":"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"}
|
|
@@ -162,9 +162,9 @@ declare const pluginManifestSchema: z.ZodObject<{
|
|
|
162
162
|
type: z.ZodLiteral<"sql_warehouse">;
|
|
163
163
|
}, z.core.$strict>, z.ZodObject<{
|
|
164
164
|
permission: z.ZodEnum<{
|
|
165
|
-
CAN_QUERY: "CAN_QUERY";
|
|
166
165
|
CAN_VIEW: "CAN_VIEW";
|
|
167
166
|
CAN_MANAGE: "CAN_MANAGE";
|
|
167
|
+
CAN_QUERY: "CAN_QUERY";
|
|
168
168
|
}>;
|
|
169
169
|
alias: z.ZodString;
|
|
170
170
|
resourceKey: z.ZodString;
|
|
@@ -683,9 +683,9 @@ declare const pluginManifestSchema: z.ZodObject<{
|
|
|
683
683
|
type: z.ZodLiteral<"sql_warehouse">;
|
|
684
684
|
}, z.core.$strict>, z.ZodObject<{
|
|
685
685
|
permission: z.ZodEnum<{
|
|
686
|
-
CAN_QUERY: "CAN_QUERY";
|
|
687
686
|
CAN_VIEW: "CAN_VIEW";
|
|
688
687
|
CAN_MANAGE: "CAN_MANAGE";
|
|
688
|
+
CAN_QUERY: "CAN_QUERY";
|
|
689
689
|
}>;
|
|
690
690
|
alias: z.ZodString;
|
|
691
691
|
resourceKey: z.ZodString;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"arrow-stream-processor.js","names":[],"sources":["../../src/stream/arrow-stream-processor.ts"],"sourcesContent":["import type { sql } from \"@databricks/sdk-experimental\";\nimport { ExecutionError, ValidationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\n\nconst logger = createLogger(\"stream:arrow\");\n\ntype ExternalLink = sql.ExternalLink;\n\n/**\n * Re-mint a chunk's pre-signed URL. DBSQL external links expire in <= 15 min,\n * so a large result whose tail chunks are reached after the earlier chunks\n * finish draining can hit an expired link; this fetches a fresh one for the\n * given chunk index. Created in the request's identity context (it captures the\n * caller's workspace client), so it stays valid even though streaming runs\n * outside that context. Returns `undefined` if the link can't be re-resolved.\n */\nexport type RefreshChunkLink = (\n chunkIndex: number,\n signal?: AbortSignal,\n) => Promise<ExternalLink | undefined>;\n\ninterface ArrowStreamOptions {\n /** Idle timeout (ms) for a single chunk: no progress → abort the download. */\n timeout: number;\n /** Attempts to establish a chunk's response before any bytes are yielded. */\n retries: number;\n}\n\nconst BACKOFF_MULTIPLIER = 1000;\n\n/**\n * Streams Arrow IPC bytes for a completed statement's EXTERNAL_LINKS chunks\n * without ever buffering a whole chunk — each chunk's response body is piped\n * through as it arrives, so peak memory is a single network read rather than\n * a full chunk (let alone the full result). No Arrow parsing on the server;\n * the client parses the concatenated IPC bytes.\n */\nexport class ArrowStreamProcessor {\n static readonly DEFAULT_TIMEOUT = 30000;\n static readonly DEFAULT_RETRIES = 3;\n\n private options: ArrowStreamOptions;\n\n constructor(options?: Partial<ArrowStreamOptions>) {\n this.options = {\n timeout: options?.timeout ?? ArrowStreamProcessor.DEFAULT_TIMEOUT,\n retries: options?.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES,\n };\n }\n\n /**\n * Stream Arrow chunks in array order, piping each chunk's response body.\n *\n * Yields network-sized pieces as they arrive (not whole chunks), so peak\n * memory is one read buffer. Chunks are fetched sequentially; the\n * concatenation of everything yielded is byte-identical to the raw Arrow\n * result, so the client parses it exactly as a buffered response.\n */\n async *streamChunks(\n chunks: ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n if (chunks.length === 0) {\n throw ValidationError.missingField(\"chunks\");\n }\n\n for (const chunk of chunks) {\n yield* this.streamChunkBody(chunk, signal, refresh);\n }\n }\n\n /**\n * Pipe one chunk's response body.\n *\n * Retry applies only while *establishing* the response (connection failure\n * or non-2xx) — once bytes have been yielded downstream we cannot re-fetch,\n * so a mid-body failure propagates and the caller aborts the response. An\n * idle timeout, reset before every read, aborts a stalled download.\n */\n private async *streamChunkBody(\n chunk: ExternalLink,\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n let externalLink = chunk.external_link;\n if (!externalLink) {\n // A missing link cannot be fixed by retrying — fail loudly.\n throw ExecutionError.statementFailed(\n `External link missing for chunk ${chunk.chunk_index}`,\n );\n }\n\n let response: Response | undefined;\n let controller: AbortController | undefined;\n let onOuterAbort: (() => void) | undefined;\n let lastError: unknown;\n\n for (let attempt = 0; attempt < this.options.retries; attempt++) {\n if (signal?.aborted) throw ExecutionError.canceled();\n\n const attemptController = new AbortController();\n const listener = () => attemptController.abort();\n signal?.addEventListener(\"abort\", listener, { once: true });\n const timer = setTimeout(\n () => attemptController.abort(),\n this.options.timeout,\n );\n\n try {\n const r = await fetch(externalLink, {\n signal: attemptController.signal,\n });\n clearTimeout(timer);\n if (!r.ok) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index}: ${r.status} ${r.statusText}`,\n );\n }\n // Keep this attempt's controller alive to drive the body read + idle\n // timeout below.\n response = r;\n controller = attemptController;\n onOuterAbort = listener;\n break;\n } catch (error) {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", listener);\n lastError = error;\n if (signal?.aborted) throw ExecutionError.canceled();\n if (attempt < this.options.retries - 1) {\n await this.delay(2 ** attempt * BACKOFF_MULTIPLIER, signal);\n // Pre-signed URLs expire (<= 15 min). Before retrying, re-mint this\n // chunk's link — a stale URL would just 403 again on the same address.\n // Only meaningful before any bytes are yielded (below), which is why\n // this lives in the establish-response loop.\n if (refresh && chunk.chunk_index != null) {\n try {\n const fresh = await refresh(chunk.chunk_index, signal);\n if (fresh?.external_link) externalLink = fresh.external_link;\n } catch (refreshError) {\n // Keep retrying the current URL; surface the original error if\n // all attempts fail.\n logger.warn(\n \"Failed to re-resolve link for chunk %s: %O\",\n chunk.chunk_index,\n refreshError,\n );\n }\n }\n }\n }\n }\n\n if (!response || !controller) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${\n lastError instanceof Error ? lastError.message : String(lastError)\n }`,\n );\n }\n\n const body = response.body;\n let idleTimer: ReturnType<typeof setTimeout> | undefined;\n try {\n if (!body) return; // empty chunk (0 rows)\n const reader = body.getReader();\n try {\n while (true) {\n // The idle timeout must cover ONLY the upstream read, never the\n // downstream `yield`. Arm it right before `reader.read()` and clear\n // it the instant the read resolves — if it stayed armed across the\n // yield, a slow client backpressuring `writeChunk` would look like an\n // upstream stall and abort a perfectly healthy download.\n idleTimer = setTimeout(\n () => controller.abort(),\n this.options.timeout,\n );\n let done: boolean;\n let value: Uint8Array | undefined;\n try {\n ({ done, value } = await reader.read());\n } finally {\n clearTimeout(idleTimer);\n }\n if (done) break;\n if (value && value.byteLength > 0) yield value;\n }\n } finally {\n // Release the stream on early exit (downstream abort / error).\n reader.cancel().catch(() => {});\n }\n } catch (error) {\n if (signal?.aborted) throw ExecutionError.canceled();\n logger.error(\n \"Failed streaming chunk %s body: %O\",\n chunk.chunk_index,\n error,\n );\n throw error instanceof ExecutionError\n ? error\n : ExecutionError.statementFailed(\n `Failed streaming chunk ${chunk.chunk_index}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n } finally {\n clearTimeout(idleTimer);\n if (onOuterAbort) signal?.removeEventListener(\"abort\", onOuterAbort);\n }\n }\n\n private delay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(ExecutionError.canceled());\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"arrow-stream-processor.js","names":[],"sources":["../../src/stream/arrow-stream-processor.ts"],"sourcesContent":["import { ExecutionError, ValidationError } from \"../errors\";\nimport { createLogger } from \"../logging/logger\";\nimport type { sql } from \"../workspace-client\";\n\nconst logger = createLogger(\"stream:arrow\");\n\ntype ExternalLink = sql.ExternalLink;\n\n/**\n * Re-mint a chunk's pre-signed URL. DBSQL external links expire in <= 15 min,\n * so a large result whose tail chunks are reached after the earlier chunks\n * finish draining can hit an expired link; this fetches a fresh one for the\n * given chunk index. Created in the request's identity context (it captures the\n * caller's workspace client), so it stays valid even though streaming runs\n * outside that context. Returns `undefined` if the link can't be re-resolved.\n */\nexport type RefreshChunkLink = (\n chunkIndex: number,\n signal?: AbortSignal,\n) => Promise<ExternalLink | undefined>;\n\ninterface ArrowStreamOptions {\n /** Idle timeout (ms) for a single chunk: no progress → abort the download. */\n timeout: number;\n /** Attempts to establish a chunk's response before any bytes are yielded. */\n retries: number;\n}\n\nconst BACKOFF_MULTIPLIER = 1000;\n\n/**\n * Streams Arrow IPC bytes for a completed statement's EXTERNAL_LINKS chunks\n * without ever buffering a whole chunk — each chunk's response body is piped\n * through as it arrives, so peak memory is a single network read rather than\n * a full chunk (let alone the full result). No Arrow parsing on the server;\n * the client parses the concatenated IPC bytes.\n */\nexport class ArrowStreamProcessor {\n static readonly DEFAULT_TIMEOUT = 30000;\n static readonly DEFAULT_RETRIES = 3;\n\n private options: ArrowStreamOptions;\n\n constructor(options?: Partial<ArrowStreamOptions>) {\n this.options = {\n timeout: options?.timeout ?? ArrowStreamProcessor.DEFAULT_TIMEOUT,\n retries: options?.retries ?? ArrowStreamProcessor.DEFAULT_RETRIES,\n };\n }\n\n /**\n * Stream Arrow chunks in array order, piping each chunk's response body.\n *\n * Yields network-sized pieces as they arrive (not whole chunks), so peak\n * memory is one read buffer. Chunks are fetched sequentially; the\n * concatenation of everything yielded is byte-identical to the raw Arrow\n * result, so the client parses it exactly as a buffered response.\n */\n async *streamChunks(\n chunks: ExternalLink[],\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n if (chunks.length === 0) {\n throw ValidationError.missingField(\"chunks\");\n }\n\n for (const chunk of chunks) {\n yield* this.streamChunkBody(chunk, signal, refresh);\n }\n }\n\n /**\n * Pipe one chunk's response body.\n *\n * Retry applies only while *establishing* the response (connection failure\n * or non-2xx) — once bytes have been yielded downstream we cannot re-fetch,\n * so a mid-body failure propagates and the caller aborts the response. An\n * idle timeout, reset before every read, aborts a stalled download.\n */\n private async *streamChunkBody(\n chunk: ExternalLink,\n signal?: AbortSignal,\n refresh?: RefreshChunkLink,\n ): AsyncGenerator<Uint8Array, void, unknown> {\n let externalLink = chunk.external_link;\n if (!externalLink) {\n // A missing link cannot be fixed by retrying — fail loudly.\n throw ExecutionError.statementFailed(\n `External link missing for chunk ${chunk.chunk_index}`,\n );\n }\n\n let response: Response | undefined;\n let controller: AbortController | undefined;\n let onOuterAbort: (() => void) | undefined;\n let lastError: unknown;\n\n for (let attempt = 0; attempt < this.options.retries; attempt++) {\n if (signal?.aborted) throw ExecutionError.canceled();\n\n const attemptController = new AbortController();\n const listener = () => attemptController.abort();\n signal?.addEventListener(\"abort\", listener, { once: true });\n const timer = setTimeout(\n () => attemptController.abort(),\n this.options.timeout,\n );\n\n try {\n const r = await fetch(externalLink, {\n signal: attemptController.signal,\n });\n clearTimeout(timer);\n if (!r.ok) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index}: ${r.status} ${r.statusText}`,\n );\n }\n // Keep this attempt's controller alive to drive the body read + idle\n // timeout below.\n response = r;\n controller = attemptController;\n onOuterAbort = listener;\n break;\n } catch (error) {\n clearTimeout(timer);\n signal?.removeEventListener(\"abort\", listener);\n lastError = error;\n if (signal?.aborted) throw ExecutionError.canceled();\n if (attempt < this.options.retries - 1) {\n await this.delay(2 ** attempt * BACKOFF_MULTIPLIER, signal);\n // Pre-signed URLs expire (<= 15 min). Before retrying, re-mint this\n // chunk's link — a stale URL would just 403 again on the same address.\n // Only meaningful before any bytes are yielded (below), which is why\n // this lives in the establish-response loop.\n if (refresh && chunk.chunk_index != null) {\n try {\n const fresh = await refresh(chunk.chunk_index, signal);\n if (fresh?.external_link) externalLink = fresh.external_link;\n } catch (refreshError) {\n // Keep retrying the current URL; surface the original error if\n // all attempts fail.\n logger.warn(\n \"Failed to re-resolve link for chunk %s: %O\",\n chunk.chunk_index,\n refreshError,\n );\n }\n }\n }\n }\n }\n\n if (!response || !controller) {\n throw ExecutionError.statementFailed(\n `Failed to download chunk ${chunk.chunk_index} after ${this.options.retries} attempts: ${\n lastError instanceof Error ? lastError.message : String(lastError)\n }`,\n );\n }\n\n const body = response.body;\n let idleTimer: ReturnType<typeof setTimeout> | undefined;\n try {\n if (!body) return; // empty chunk (0 rows)\n const reader = body.getReader();\n try {\n while (true) {\n // The idle timeout must cover ONLY the upstream read, never the\n // downstream `yield`. Arm it right before `reader.read()` and clear\n // it the instant the read resolves — if it stayed armed across the\n // yield, a slow client backpressuring `writeChunk` would look like an\n // upstream stall and abort a perfectly healthy download.\n idleTimer = setTimeout(\n () => controller.abort(),\n this.options.timeout,\n );\n let done: boolean;\n let value: Uint8Array | undefined;\n try {\n ({ done, value } = await reader.read());\n } finally {\n clearTimeout(idleTimer);\n }\n if (done) break;\n if (value && value.byteLength > 0) yield value;\n }\n } finally {\n // Release the stream on early exit (downstream abort / error).\n reader.cancel().catch(() => {});\n }\n } catch (error) {\n if (signal?.aborted) throw ExecutionError.canceled();\n logger.error(\n \"Failed streaming chunk %s body: %O\",\n chunk.chunk_index,\n error,\n );\n throw error instanceof ExecutionError\n ? error\n : ExecutionError.statementFailed(\n `Failed streaming chunk ${chunk.chunk_index}: ${\n error instanceof Error ? error.message : String(error)\n }`,\n );\n } finally {\n clearTimeout(idleTimer);\n if (onOuterAbort) signal?.removeEventListener(\"abort\", onOuterAbort);\n }\n }\n\n private delay(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n const onAbort = () => {\n clearTimeout(timer);\n reject(ExecutionError.canceled());\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -94,7 +94,65 @@ function isConnectivityError(error) {
|
|
|
94
94
|
}
|
|
95
95
|
return false;
|
|
96
96
|
}
|
|
97
|
+
const AUTH_ERROR_STATUSES = new Set([401, 403]);
|
|
98
|
+
/**
|
|
99
|
+
* Classifies a thrown failure into one of two buckets: deterministic failures
|
|
100
|
+
* that must be surfaced (bad warehouse id, malformed request) or environmental
|
|
101
|
+
* issues (connectivity, auth, deleted warehouse, timeouts) that the has-types
|
|
102
|
+
* gate will handle later.
|
|
103
|
+
*
|
|
104
|
+
* Returns:
|
|
105
|
+
* - "deterministic": HTTP 404 (bad warehouse id) or 400 (malformed request).
|
|
106
|
+
* The build must fail.
|
|
107
|
+
* - "environmental": Everything else — auth (401/403), connectivity errors,
|
|
108
|
+
* warehouse state changes (DELETED/DELETING), wait-for-RUNNING timeouts,
|
|
109
|
+
* unrecognized failures. Default = environmental.
|
|
110
|
+
*
|
|
111
|
+
* Walks `cause`/`AggregateError` chains when checking for deterministic status,
|
|
112
|
+
* so a wrapped 404 is still recognized as deterministic.
|
|
113
|
+
*/
|
|
114
|
+
function classifyBlockingFailure(error) {
|
|
115
|
+
const seen = /* @__PURE__ */ new Set();
|
|
116
|
+
const stack = [error];
|
|
117
|
+
while (stack.length > 0) {
|
|
118
|
+
const current = stack.pop();
|
|
119
|
+
if (current === void 0 || seen.has(current)) continue;
|
|
120
|
+
seen.add(current);
|
|
121
|
+
const status = getErrorStatus(current);
|
|
122
|
+
if (status === 404 || status === 400) return "deterministic";
|
|
123
|
+
stack.push(...getErrorChildren(current));
|
|
124
|
+
}
|
|
125
|
+
const topLevelStatus = getErrorStatus(error);
|
|
126
|
+
if (topLevelStatus !== void 0 && AUTH_ERROR_STATUSES.has(topLevelStatus)) return "environmental";
|
|
127
|
+
if (isConnectivityError(error)) return "environmental";
|
|
128
|
+
return "environmental";
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Coarse cause label for an environmental failure, used by the `--wait`
|
|
132
|
+
* committed-types warning so the log says *why* generation fell back.
|
|
133
|
+
*
|
|
134
|
+
* Returns:
|
|
135
|
+
* - "unreachable": transport/connectivity failure (see {@link isConnectivityError}).
|
|
136
|
+
* - "auth": HTTP 401/403, including a status carried on `response.status` or
|
|
137
|
+
* wrapped in a `cause`/`AggregateError` chain.
|
|
138
|
+
* - "unavailable": everything else (DELETED/DELETING, wait timeouts, degraded
|
|
139
|
+
* DESCRIBEs).
|
|
140
|
+
*/
|
|
141
|
+
function classifyEnvironmentalCause(error) {
|
|
142
|
+
if (isConnectivityError(error)) return "unreachable";
|
|
143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
144
|
+
const stack = [error];
|
|
145
|
+
while (stack.length > 0) {
|
|
146
|
+
const current = stack.pop();
|
|
147
|
+
if (current === void 0 || seen.has(current)) continue;
|
|
148
|
+
seen.add(current);
|
|
149
|
+
const status = getErrorStatus(current);
|
|
150
|
+
if (status !== void 0 && AUTH_ERROR_STATUSES.has(status)) return "auth";
|
|
151
|
+
stack.push(...getErrorChildren(current));
|
|
152
|
+
}
|
|
153
|
+
return "unavailable";
|
|
154
|
+
}
|
|
97
155
|
|
|
98
156
|
//#endregion
|
|
99
|
-
export { getErrorDiagnostic, getErrorMessage, isConnectivityError };
|
|
157
|
+
export { classifyBlockingFailure, classifyEnvironmentalCause, getErrorDiagnostic, getErrorMessage, isConnectivityError };
|
|
100
158
|
//# sourceMappingURL=errors.js.map
|
|
@@ -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"],"mappings":";;;;;;;;;;;;AAYA,SAAS,SAAS,OAAkD;AAClE,QAAO,OAAO,UAAU,YAAY,UAAU;;AAGhD,SAAgB,gBAAgB,OAAwB;AACtD,KAAI,iBAAiB,MAAO,QAAO,MAAM;AACzC,KAAI,SAAS,MAAM,IAAI,OAAO,MAAM,YAAY,SAC9C,QAAO,MAAM;AAEf,QAAO,OAAO,MAAM;;AAGtB,SAAgB,mBAAmB,OAAwB;CACzD,MAAM,uBAAO,IAAI,KAAc;CAC/B,MAAM,WAAqB,EAAE;CAC7B,MAAM,QAAQ,CAAC,MAAM;AAErB,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,YAAY,UAAa,KAAK,IAAI,QAAQ,CAAE;AAChD,OAAK,IAAI,QAAQ;EAEjB,MAAM,UAAU,gBAAgB,QAAQ;AACxC,MACE,WACA,YAAY,qBACZ,CAAC,SAAS,SAAS,QAAQ,CAE3B,UAAS,KAAK,QAAQ;EAGxB,MAAM,OAAO,aAAa,QAAQ;AAClC,MAAI,QAAQ,CAAC,SAAS,SAAS,KAAK,CAAE,UAAS,KAAK,KAAK;AAEzD,QAAM,KAAK,GAAG,iBAAiB,QAAQ,CAAC;;AAG1C,QAAO,SAAS,SAAS,IAAI,SAAS,KAAK,KAAK,GAAG,gBAAgB,MAAM;;AAG3E,SAAS,aAAa,OAAoC;AACxD,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,OAAO,MAAM,QAAQ,MAAM;AACjC,QAAO,OAAO,SAAS,WAAW,OAAO;;AAG3C,SAAS,eAAe,OAAoC;AAC1D,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO;CAC7B,MAAM,SAAS,MAAM,UAAU,MAAM;AACrC,KAAI,OAAO,WAAW,SAAU,QAAO;AACvC,KAAI,SAAS,MAAM,SAAS,IAAI,OAAO,MAAM,SAAS,WAAW,SAC/D,QAAO,MAAM,SAAS;;AAK1B,SAAS,iBAAiB,OAA2B;AACnD,KAAI,CAAC,SAAS,MAAM,CAAE,QAAO,EAAE;CAC/B,MAAM,WAAsB,EAAE;AAC9B,KAAI,WAAW,MAAO,UAAS,KAAK,MAAM,MAAM;AAChD,KAAI,iBAAiB,eAAgB,UAAS,KAAK,GAAG,MAAM,OAAO;AACnE,QAAO;;AAGT,MAAM,2BAA2B,IAAI,IAAI;CACvC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;AAEF,SAAS,sBAAsB,SAA0B;AACvD,QACE,8CAA8C,KAAK,QAAQ,IAC3D,sBAAsB,KAAK,QAAQ,IACnC,qBAAqB,KAAK,QAAQ,IAClC,yBAAyB,KAAK,QAAQ,IACtC,+BAA+B,KAAK,QAAQ,IAC5C,8CAA8C,KAAK,QAAQ,IAC3D,kEAAkE,KAChE,QACD;;;;;;;;;AAWL,SAAgB,oBAAoB,OAAyB;CAC3D,MAAM,uBAAO,IAAI,KAAc;CAC/B,MAAM,QAAQ,CAAC,MAAM;AAErB,QAAO,MAAM,SAAS,GAAG;EACvB,MAAM,UAAU,MAAM,KAAK;AAC3B,MAAI,YAAY,UAAa,KAAK,IAAI,QAAQ,CAAE;AAChD,OAAK,IAAI,QAAQ;EAEjB,MAAM,OAAO,aAAa,QAAQ;AAClC,MACE,SACC,yBAAyB,IAAI,KAAK,IAAI,KAAK,WAAW,WAAW,EAElE,QAAO;EAGT,MAAM,SAAS,eAAe,QAAQ;AACtC,MAAI,WAAW,OAAO,WAAW,OAAO,WAAW,IAAK,QAAO;AAE/D,MAAI,sBAAsB,gBAAgB,QAAQ,CAAC,CAAE,QAAO;AAE5D,QAAM,KAAK,GAAG,iBAAiB,QAAQ,CAAC;;AAG1C,QAAO"}
|
|
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 // Deterministic: check first so they're never swallowed by environmental rules.\n // Walk the error chain to find any deterministic status.\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 // Environmental: auth, connectivity, unrecognized, default.\n const topLevelStatus = getErrorStatus(error);\n if (topLevelStatus !== undefined && AUTH_ERROR_STATUSES.has(topLevelStatus)) {\n return \"environmental\";\n }\n\n if (isConnectivityError(error)) {\n return \"environmental\";\n }\n\n // Default: any unrecognized failure or no status (DELETED/DELETING messages,\n // timeout messages, plain Error objects) → environmental.\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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"}
|