diff --git a/apps/dev-playground/client/src/routes/jobs.route.tsx b/apps/dev-playground/client/src/routes/jobs.route.tsx index efcb945b1..b57b4931c 100644 --- a/apps/dev-playground/client/src/routes/jobs.route.tsx +++ b/apps/dev-playground/client/src/routes/jobs.route.tsx @@ -9,10 +9,10 @@ export const Route = createFileRoute("/jobs")({ const MAX_STREAM_LOG = 500; interface Run { - run_id: number; - state?: { life_cycle_state?: string; result_state?: string }; - start_time?: number; - end_time?: number; + runId: number; + state?: { lifeCycleState?: string; resultState?: string }; + startTime?: number; + endTime?: number; } function formatTime(ts?: number) { @@ -211,8 +211,8 @@ function JobsRoute() {

{status.run && (

- Run #{status.run.run_id} — started{" "} - {formatTime(status.run.start_time)} + Run #{status.run.runId} — started{" "} + {formatTime(status.run.startTime)}

)} @@ -235,27 +235,27 @@ function JobsRoute() { {runs.map((run) => ( - - {run.run_id} + + {run.runId} - {run.state?.life_cycle_state ?? "—"} + {run.state?.lifeCycleState ?? "—"} - {run.state?.result_state ?? "—"} + {run.state?.resultState ?? "—"} - {formatTime(run.start_time)} + {formatTime(run.startTime)} - {run.state?.life_cycle_state === "RUNNING" && ( + {run.state?.lifeCycleState === "RUNNING" && ( diff --git a/docs/docs/plugins/jobs.md b/docs/docs/plugins/jobs.md index d736dcfb7..97d97c5ab 100644 --- a/docs/docs/plugins/jobs.md +++ b/docs/docs/plugins/jobs.md @@ -97,11 +97,11 @@ When `taskType` is set, the plugin maps validated parameters to the correct SDK | Task type | SDK field | Parameter shape | |-----------|-----------|-----------------| -| `notebook` | `notebook_params` | `Record` — values coerced to string | -| `python_wheel` | `python_named_params` | `Record` — values coerced to string | -| `python_script` | `python_params` | `{ args: string[] }` — positional args | -| `spark_jar` | `jar_params` | `{ args: string[] }` — positional args | -| `sql` | `sql_params` | `Record` — values coerced to string | +| `notebook` | `notebookParams` | `Record` — values coerced to string | +| `python_wheel` | `pythonNamedParams` | `Record` — values coerced to string | +| `python_script` | `pythonParams` | `{ args: string[] }` — positional args | +| `spark_jar` | `jarParams` | `{ args: string[] }` — positional args | +| `sql` | `sqlParams` | `Record` — values coerced to string | | `dbt` | — | No parameters accepted | ```ts @@ -198,7 +198,7 @@ const etl = AppKit.jobs("etl"); // Trigger a run const result = await etl.runNow({ startDate: "2025-01-01" }); if (result.ok) { - console.log("Run ID:", result.data.run_id); + console.log("Run ID:", result.data.runId); } // Trigger and poll until completion diff --git a/knip.json b/knip.json index c57c8be24..14df12e42 100644 --- a/knip.json +++ b/knip.json @@ -14,6 +14,7 @@ "@databricks/sdk-core", "@databricks/sdk-experimental", "@databricks/sdk-genie", + "@databricks/sdk-jobs", "@databricks/sdk-options", "@databricks/sdk-scim", "@databricks/sdk-statementexecution", diff --git a/packages/appkit/package.json b/packages/appkit/package.json index 3eda04dba..33899de06 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -75,6 +75,7 @@ "@databricks/sdk-core": "0.51.0", "@databricks/sdk-experimental": "0.17.0", "@databricks/sdk-genie": "0.54.0", + "@databricks/sdk-jobs": "0.57.0", "@databricks/sdk-options": "0.51.0", "@databricks/sdk-scim": "0.51.0", "@databricks/sdk-statementexecution": "0.52.0", diff --git a/packages/appkit/src/connectors/jobs/client.ts b/packages/appkit/src/connectors/jobs/client.ts index a00474d21..dbf94ca71 100644 --- a/packages/appkit/src/connectors/jobs/client.ts +++ b/packages/appkit/src/connectors/jobs/client.ts @@ -9,15 +9,44 @@ import { SpanStatusCode, TelemetryManager, } from "../../telemetry"; -import { - Context, - type jobs, - type WorkspaceClient, +import { isPlainObject } from "../../utils/is-plain-object"; +import type { + CancelRunRequest, + GetJobRequest, + GetRunOutputRequest, + GetRunRequest, + ListRunsRequest, + RunNowRequest, + SubmitRunRequest, + WorkspaceClient, } from "../../workspace-client"; -import type { JobsConnectorConfig } from "./types"; +import type { + JobBaseRun, + JobDetails, + JobRun, + JobRunNowResponse, + JobRunOutput, + JobsConnectorConfig, + JobSubmitRunResponse, + Numeric, +} from "./types"; const logger = createLogger("connectors:jobs"); +/** + * The modular SDK types int64s as `bigint`, which `JSON.stringify` rejects, and + * every jobs response ends up in HTTP JSON, SSE or the cache. `Number()` loses + * precision past 2^53, as the legacy SDK's plain `JSON.parse` did. + */ +function toNumbers(value: T): Numeric { + if (typeof value === "bigint") return Number(value) as Numeric; + if (Array.isArray(value)) return value.map(toNumbers) as Numeric; + if (!isPlainObject(value)) return value as Numeric; + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, toNumbers(v)]), + ) as Numeric; +} + export class JobsConnector { private readonly name = "jobs"; private readonly config: JobsConnectorConfig; @@ -51,73 +80,69 @@ export class JobsConnector { async submitRun( workspaceClient: WorkspaceClient, - request: jobs.SubmitRun, + request: SubmitRunRequest, signal?: AbortSignal, - ): Promise { + ): Promise { return this._callApi("submit", async () => { - return workspaceClient.jobs.submit(request, this._createContext(signal)); + const waiter = await workspaceClient.jobs.submitRun(request, { signal }); + return { runId: Number(waiter.runId) }; }); } async runNow( workspaceClient: WorkspaceClient, - request: jobs.RunNow, + request: RunNowRequest, signal?: AbortSignal, - ): Promise { + ): Promise { return this._callApi("runNow", async () => { - return workspaceClient.jobs.runNow(request, this._createContext(signal)); + const waiter = await workspaceClient.jobs.runNow(request, { signal }); + return { runId: Number(waiter.runId) }; }); } async getRun( workspaceClient: WorkspaceClient, - request: jobs.GetRunRequest, + request: GetRunRequest, signal?: AbortSignal, - ): Promise { - return this._callApi("getRun", async () => { - return workspaceClient.jobs.getRun(request, this._createContext(signal)); - }); + ): Promise { + return this._callApi("getRun", async () => + toNumbers(await workspaceClient.jobs.getRun(request, { signal })), + ); } async getRunOutput( workspaceClient: WorkspaceClient, - request: jobs.GetRunOutputRequest, + request: GetRunOutputRequest, signal?: AbortSignal, - ): Promise { - return this._callApi("getRunOutput", async () => { - return workspaceClient.jobs.getRunOutput( - request, - this._createContext(signal), - ); - }); + ): Promise { + return this._callApi("getRunOutput", async () => + toNumbers(await workspaceClient.jobs.getRunOutput(request, { signal })), + ); } async cancelRun( workspaceClient: WorkspaceClient, - request: jobs.CancelRun, + request: CancelRunRequest, signal?: AbortSignal, ): Promise { await this._callApi("cancelRun", async () => { - return workspaceClient.jobs.cancelRun( - request, - this._createContext(signal), - ); + await workspaceClient.jobs.cancelRun(request, { signal }); }); } async listRuns( workspaceClient: WorkspaceClient, - request: jobs.ListRunsRequest, + request: ListRunsRequest, signal?: AbortSignal, - ): Promise { + ): Promise { return this._callApi("listRuns", async () => { - const runs: jobs.BaseRun[] = []; + const runs: JobBaseRun[] = []; const limit = Math.max(1, Math.min(request.limit ?? 100, 100)); - for await (const run of workspaceClient.jobs.listRuns( + for await (const run of workspaceClient.jobs.listRunsIter( { ...request, limit }, - this._createContext(signal), + { signal }, )) { - runs.push(run); + runs.push(toNumbers(run)); if (runs.length >= limit) break; } return runs; @@ -126,12 +151,12 @@ export class JobsConnector { async getJob( workspaceClient: WorkspaceClient, - request: jobs.GetJobRequest, + request: GetJobRequest, signal?: AbortSignal, - ): Promise { - return this._callApi("getJob", async () => { - return workspaceClient.jobs.get(request, this._createContext(signal)); - }); + ): Promise { + return this._callApi("getJob", async () => + toNumbers(await workspaceClient.jobs.getJob(request, { signal })), + ); } private async _callApi( @@ -164,6 +189,17 @@ export class JobsConnector { if (error instanceof AppKitError) { throw error; } + // The modular SDK's ApiError exposes the HTTP status as + // `httpStatusCode` (-1 when not an HTTP error); Plugin.execute() + // maps on `statusCode`. + if ( + error instanceof Error && + "httpStatusCode" in error && + typeof error.httpStatusCode === "number" && + error.httpStatusCode > 0 + ) { + throw Object.assign(error, { statusCode: error.httpStatusCode }); + } // Preserve SDK ApiError (and any error with a numeric statusCode) // so Plugin.execute() can map it to the correct HTTP status. if ( @@ -198,19 +234,4 @@ export class JobsConnector { { name: this.name, includePrefix: true }, ); } - - private _createContext(signal?: AbortSignal) { - return new Context({ - cancellationToken: { - // Getter — evaluated on every read so SDK code paths that poll - // (rather than subscribe) observe cancellation live. - get isCancellationRequested() { - return signal?.aborted ?? false; - }, - onCancellationRequested: (cb: () => void) => { - signal?.addEventListener("abort", cb, { once: true }); - }, - }, - }); - } } diff --git a/packages/appkit/src/connectors/jobs/index.ts b/packages/appkit/src/connectors/jobs/index.ts index efb4753a8..4a31519fa 100644 --- a/packages/appkit/src/connectors/jobs/index.ts +++ b/packages/appkit/src/connectors/jobs/index.ts @@ -1,2 +1,9 @@ export { JobsConnector } from "./client"; -export type { JobsConnectorConfig } from "./types"; +export type { + JobBaseRun, + JobDetails, + JobRun, + JobRunNowResponse, + JobRunOutput, + JobsConnectorConfig, +} from "./types"; diff --git a/packages/appkit/src/connectors/jobs/tests/client.test.ts b/packages/appkit/src/connectors/jobs/tests/client.test.ts new file mode 100644 index 000000000..4ade28d39 --- /dev/null +++ b/packages/appkit/src/connectors/jobs/tests/client.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, test, vi } from "vitest"; + +import { JobsConnector } from "../client"; + +// The modular Jobs SDK types int64s as `bigint`, which `JSON.stringify` rejects. +// Every connector response ends up in HTTP JSON, SSE or the cache. +describe("JobsConnector", () => { + test("getRun returns a JSON-serializable run with numeric int64s", async () => { + const getRun = vi.fn().mockResolvedValue({ + runId: 9007199254740991n, + jobId: 123n, + startTime: 1700000000000n, + state: { lifeCycleState: "TERMINATED" }, + tasks: [{ taskKey: "t1", attemptNumber: 0n }], + }); + const client = { jobs: { getRun } } as never; + + const run = await new JobsConnector({}).getRun(client, { runId: 42n }); + + expect(getRun).toHaveBeenCalledWith({ runId: 42n }, { signal: undefined }); + expect(JSON.parse(JSON.stringify(run))).toEqual({ + runId: 9007199254740991, + jobId: 123, + startTime: 1700000000000, + state: { lifeCycleState: "TERMINATED" }, + tasks: [{ taskKey: "t1", attemptNumber: 0 }], + }); + }); + + test("runNow returns the waiter's runId as a number", async () => { + const runNow = vi.fn().mockResolvedValue({ runId: 7n }); + const client = { jobs: { runNow } } as never; + + const result = await new JobsConnector({}).runNow(client, { jobId: 123n }); + + expect(result).toEqual({ runId: 7 }); + }); + + test("surfaces the modular ApiError's httpStatusCode as statusCode", async () => { + // Shaped like sdk-core's ApiError: the status is a getter, not `statusCode`. + class ModularApiError extends Error { + get httpStatusCode() { + return 404; + } + } + const getRun = vi.fn().mockRejectedValue(new ModularApiError("not found")); + const client = { jobs: { getRun } } as never; + + await expect( + new JobsConnector({}).getRun(client, { runId: 1n }), + ).rejects.toMatchObject({ statusCode: 404, message: "not found" }); + }); +}); diff --git a/packages/appkit/src/connectors/jobs/types.ts b/packages/appkit/src/connectors/jobs/types.ts index dd7dfbbe5..e5b71d3d7 100644 --- a/packages/appkit/src/connectors/jobs/types.ts +++ b/packages/appkit/src/connectors/jobs/types.ts @@ -1,5 +1,33 @@ import type { TelemetryOptions } from "shared"; +import type { + BaseRun, + GetJobResponse, + GetRunOutputResponse, + GetRunResponse, + RunNowResponse, + SubmitRunResponse, +} from "../../workspace-client"; + export interface JobsConnectorConfig { telemetry?: TelemetryOptions; } + +/** A modular SDK model with its `bigint` int64s as `number` (JSON-safe). */ +export type Numeric = T extends bigint + ? number + : // Before the object branch: SDK enums are `"X" | (string & {})`. + T extends string | number | boolean | null | undefined + ? T + : T extends readonly (infer U)[] + ? Numeric[] + : T extends object + ? { [K in keyof T]: Numeric } + : T; + +export type JobRun = Numeric; +export type JobBaseRun = Numeric; +export type JobRunOutput = Numeric; +export type JobDetails = Numeric; +export type JobRunNowResponse = Numeric; +export type JobSubmitRunResponse = Numeric; diff --git a/packages/appkit/src/plugins/jobs/params.ts b/packages/appkit/src/plugins/jobs/params.ts index 757107446..75b7545bb 100644 --- a/packages/appkit/src/plugins/jobs/params.ts +++ b/packages/appkit/src/plugins/jobs/params.ts @@ -21,36 +21,34 @@ export function mapParams( ): Record { switch (taskType) { case "notebook": - // notebook_params expects Record, values coerced to string + // notebookParams expects Record, values coerced to string assertPrimitiveValues(params); return { - notebook_params: Object.fromEntries( + notebookParams: Object.fromEntries( Object.entries(params).map(([k, v]) => [k, String(v)]), ), }; case "python_wheel": assertPrimitiveValues(params); return { - python_named_params: Object.fromEntries( + pythonNamedParams: Object.fromEntries( Object.entries(params).map(([k, v]) => [k, String(v)]), ), }; case "python_script": - // python_params expects string[] (positional args) + // pythonParams expects string[] (positional args) return { - python_params: Array.isArray(params.args) - ? params.args.map(String) - : [], + pythonParams: Array.isArray(params.args) ? params.args.map(String) : [], }; case "spark_jar": - // jar_params expects string[] + // jarParams expects string[] return { - jar_params: Array.isArray(params.args) ? params.args.map(String) : [], + jarParams: Array.isArray(params.args) ? params.args.map(String) : [], }; case "sql": assertPrimitiveValues(params); return { - sql_params: Object.fromEntries( + sqlParams: Object.fromEntries( Object.entries(params).map(([k, v]) => [k, String(v)]), ), }; diff --git a/packages/appkit/src/plugins/jobs/plugin.ts b/packages/appkit/src/plugins/jobs/plugin.ts index d9f4913b1..084df58d2 100644 --- a/packages/appkit/src/plugins/jobs/plugin.ts +++ b/packages/appkit/src/plugins/jobs/plugin.ts @@ -8,7 +8,14 @@ import type { } from "shared"; import { toJSONSchema } from "zod"; -import { JobsConnector } from "../../connectors/jobs"; +import { + type JobBaseRun, + type JobDetails, + type JobRun, + type JobRunNowResponse, + type JobRunOutput, + JobsConnector, +} from "../../connectors/jobs"; import { getCurrentPrincipalId, getWorkspaceClient } from "../../context"; import { ExecutionError, ValidationError } from "../../errors"; import { createLogger } from "../../logging/logger"; @@ -16,7 +23,6 @@ import type { ExecutionResult } from "../../plugin"; import { Plugin, toPlugin } from "../../plugin"; import type { ResourceRequirement } from "../../registry"; import { defineManifest, ResourceType } from "../../registry"; -import type { jobs as jobsTypes } from "../../workspace-client"; import { JOBS_READ_DEFAULTS, JOBS_STREAM_DEFAULTS, @@ -270,26 +276,26 @@ class JobsPlugin extends Plugin { ): Promise | null> => { const result = await self.execute( async (signal) => - self.connector.getRun(client, { run_id: runId }, signal), + self.connector.getRun(client, { runId: BigInt(runId) }, signal), self._readSettings(["jobs:getRun", jobKey, runId]), userKey, ); if (!result.ok) return errorResult(result.status); - if (result.data.job_id !== jobId) return errorResult(404); + if (result.data.jobId !== jobId) return errorResult(404); return null; }; return { runNow: async ( params?: Record, - ): Promise> => { + ): Promise> => { const sdkFields = self._validateAndMap(jobKey, params); const result = await self.execute( async (signal) => self.connector.runNow( client, - { ...sdkFields, job_id: jobId }, + { ...sdkFields, jobId: BigInt(jobId) }, signal, ), self._writeSettings(), @@ -308,7 +314,7 @@ class JobsPlugin extends Plugin { async (signal) => self.connector.runNow( client, - { ...sdkFields, job_id: jobId }, + { ...sdkFields, jobId: BigInt(jobId) }, signal, ), self._writeSettings(), @@ -318,9 +324,9 @@ class JobsPlugin extends Plugin { if (!runResult.ok) { throw new ExecutionError("Failed to trigger job run"); } - const runId = runResult.data.run_id; + const runId = runResult.data.runId; if (!runId) { - throw new Error("runNow did not return a run_id"); + throw new Error("runNow did not return a runId"); } const basePollInterval = @@ -339,7 +345,7 @@ class JobsPlugin extends Plugin { const runStatusResult = await self.execute( async (signal) => - self.connector.getRun(client, { run_id: runId }, signal), + self.connector.getRun(client, { runId: BigInt(runId) }, signal), { default: { ...JOBS_READ_DEFAULTS, @@ -354,7 +360,7 @@ class JobsPlugin extends Plugin { ); } const run = runStatusResult.data; - const state = run.state?.life_cycle_state; + const state = run.state?.lifeCycleState; yield { status: state, timestamp: Date.now(), run }; @@ -369,14 +375,12 @@ class JobsPlugin extends Plugin { } }, - lastRun: async (): Promise< - ExecutionResult - > => { + lastRun: async (): Promise> => { const result = await self.execute( async (signal) => self.connector.listRuns( client, - { job_id: jobId, limit: 1 }, + { jobId: BigInt(jobId), limit: 1 }, signal, ), self._readSettings(["jobs:lastRun", jobKey]), @@ -388,12 +392,12 @@ class JobsPlugin extends Plugin { listRuns: async (options?: { limit?: number; - }): Promise> => { + }): Promise> => { const result = await self.execute( async (signal) => self.connector.listRuns( client, - { job_id: jobId, limit: options?.limit }, + { jobId: BigInt(jobId), limit: options?.limit }, signal, ), self._readSettings([ @@ -406,28 +410,30 @@ class JobsPlugin extends Plugin { return result.ok ? result : errorResult(result.status); }, - getRun: async ( - runId: number, - ): Promise> => { + getRun: async (runId: number): Promise> => { const result = await self.execute( async (signal) => - self.connector.getRun(client, { run_id: runId }, signal), + self.connector.getRun(client, { runId: BigInt(runId) }, signal), self._readSettings(["jobs:getRun", jobKey, runId]), userKey, ); if (!result.ok) return errorResult(result.status); - if (result.data.job_id !== jobId) return errorResult(404); + if (result.data.jobId !== jobId) return errorResult(404); return result; }, getRunOutput: async ( runId: number, - ): Promise> => { + ): Promise> => { const scopeError = await verifyRunScope(runId); if (scopeError) return scopeError; const result = await self.execute( async (signal) => - self.connector.getRunOutput(client, { run_id: runId }, signal), + self.connector.getRunOutput( + client, + { runId: BigInt(runId) }, + signal, + ), self._readSettings(["jobs:getRunOutput", jobKey, runId]), userKey, ); @@ -439,17 +445,17 @@ class JobsPlugin extends Plugin { if (scopeError) return scopeError; const result = await self.execute( async (signal) => - self.connector.cancelRun(client, { run_id: runId }, signal), + self.connector.cancelRun(client, { runId: BigInt(runId) }, signal), self._writeSettings(), userKey, ); return result.ok ? result : errorResult(result.status); }, - getJob: async (): Promise> => { + getJob: async (): Promise> => { const result = await self.execute( async (signal) => - self.connector.getJob(client, { job_id: jobId }, signal), + self.connector.getJob(client, { jobId: BigInt(jobId) }, signal), self._readSettings(["jobs:getJob", jobKey]), userKey, ); @@ -567,7 +573,7 @@ class JobsPlugin extends Plugin { this._sendStatusError(res, result.status); return; } - res.json({ runId: result.data.run_id }); + res.json({ runId: result.data.runId }); } } catch (error) { if (error instanceof ValidationError) { @@ -674,7 +680,7 @@ class JobsPlugin extends Plugin { return; } res.json({ - status: result.data?.state?.life_cycle_state ?? null, + status: result.data?.state?.lifeCycleState ?? null, run: result.data ?? null, }); } catch (error) { diff --git a/packages/appkit/src/plugins/jobs/tests/plugin.test.ts b/packages/appkit/src/plugins/jobs/tests/plugin.test.ts index 84b44ed0c..4eb25d1b0 100644 --- a/packages/appkit/src/plugins/jobs/tests/plugin.test.ts +++ b/packages/appkit/src/plugins/jobs/tests/plugin.test.ts @@ -24,29 +24,22 @@ const { mockClient, jobsApi } = await vi.hoisted(async () => { const mockClient = createMockWorkspaceClient(); - // Facade accessors are typed against the legacy SDK, so `.mockResolvedValue` + // Facade accessors are typed against the SDK, so `.mockResolvedValue` // on them would not typecheck. `getMock` is the typed handle; it mints // idempotently, so these are the very functions the plugin will call. const jobsApi = { runNow: getMock(mockClient, "jobs.runNow"), - submit: getMock(mockClient, "jobs.submit"), + submitRun: getMock(mockClient, "jobs.submitRun"), getRun: getMock(mockClient, "jobs.getRun"), getRunOutput: getMock(mockClient, "jobs.getRunOutput"), cancelRun: getMock(mockClient, "jobs.cancelRun"), - listRuns: getMock(mockClient, "jobs.listRuns"), - get: getMock(mockClient, "jobs.get"), + listRunsIter: getMock(mockClient, "jobs.listRunsIter"), + getJob: getMock(mockClient, "jobs.getJob"), }; return { mockClient, jobsApi }; }); -vi.mock("../../../workspace-client", async (importOriginal) => { - const actual = - await importOriginal(); - // Only `Context` — the client itself is injected through ServiceContext. - return { ...actual, Context: vi.fn() }; -}); - // Boots AppKit's real in-memory cache (no cache-module mock needed). useTestCache(); @@ -264,7 +257,7 @@ describe("JobsPlugin", () => { test("runNow passes configured job_id to connector", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({}); const exported = plugin.exports(); @@ -273,7 +266,7 @@ describe("JobsPlugin", () => { await handle.runNow(); expect(jobsApi.runNow).toHaveBeenCalledWith( - expect.objectContaining({ job_id: 123 }), + expect.objectContaining({ jobId: 123n }), expect.anything(), ); }); @@ -281,20 +274,20 @@ describe("JobsPlugin", () => { test("runNow merges user params with configured job_id (no taskType)", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({}); const exported = plugin.exports(); const handle = exported("etl"); await handle.runNow({ - notebook_params: { key: "value" }, + notebookParams: { key: "value" }, }); expect(jobsApi.runNow).toHaveBeenCalledWith( expect.objectContaining({ - job_id: 123, - notebook_params: { key: "value" }, + jobId: 123n, + notebookParams: { key: "value" }, }), expect.anything(), ); @@ -323,7 +316,7 @@ describe("JobsPlugin", () => { test("runNow maps validated params to SDK fields when taskType is set", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({ jobs: { @@ -339,8 +332,8 @@ describe("JobsPlugin", () => { expect(jobsApi.runNow).toHaveBeenCalledWith( expect.objectContaining({ - job_id: 123, - notebook_params: { key: "value" }, + jobId: 123n, + notebookParams: { key: "value" }, }), expect.anything(), ); @@ -349,7 +342,7 @@ describe("JobsPlugin", () => { test("runNow skips validation when no schema is configured", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({}); const handle = plugin.exports()("etl"); @@ -363,8 +356,8 @@ describe("JobsPlugin", () => { process.env.DATABRICKS_JOB_ETL = "123"; jobsApi.getRun.mockResolvedValue({ - run_id: 1, - state: { life_cycle_state: "TERMINATED" }, + runId: 1n, + state: { lifeCycleState: "TERMINATED" }, }); const plugin = new JobsPlugin({}); @@ -389,7 +382,7 @@ describe("JobsPlugin", () => { test("getJob wraps call in execute", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.get.mockResolvedValue({ job_id: 123 }); + jobsApi.getJob.mockResolvedValue({ jobId: 123n }); const plugin = new JobsPlugin({}); const executeSpy = vi.spyOn(plugin as any, "execute"); @@ -413,7 +406,7 @@ describe("JobsPlugin", () => { test("listRuns clamps caller-supplied limit before calling the SDK", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.listRuns.mockReturnValue((async function* () {})()); + jobsApi.listRunsIter.mockReturnValue((async function* () {})()); const plugin = new JobsPlugin({}); const handle = plugin.exports()("etl"); @@ -421,7 +414,7 @@ describe("JobsPlugin", () => { await handle.listRuns({ limit: 10000 }); // SDK should receive the clamped limit, not the caller-supplied 10000. - expect(jobsApi.listRuns).toHaveBeenCalledWith( + expect(jobsApi.listRunsIter).toHaveBeenCalledWith( expect.objectContaining({ limit: 100 }), expect.anything(), ); @@ -431,7 +424,7 @@ describe("JobsPlugin", () => { process.env.DATABRICKS_JOB_ETL = "123"; // Pre-flight getRun verifies the run belongs to the configured jobId. - jobsApi.getRun.mockResolvedValue({ run_id: 1, job_id: 123 }); + jobsApi.getRun.mockResolvedValue({ runId: 1n, jobId: 123n }); jobsApi.cancelRun.mockResolvedValue(undefined); const plugin = new JobsPlugin({}); @@ -452,15 +445,15 @@ describe("JobsPlugin", () => { test("runAndWait yields status updates and terminates on TERMINATED", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); jobsApi.getRun .mockResolvedValueOnce({ - run_id: 42, - state: { life_cycle_state: "RUNNING" }, + runId: 42n, + state: { lifeCycleState: "RUNNING" }, }) .mockResolvedValueOnce({ - run_id: 42, - state: { life_cycle_state: "TERMINATED" }, + runId: 42n, + state: { lifeCycleState: "TERMINATED" }, }); const plugin = new JobsPlugin({ pollIntervalMs: 10 }); @@ -485,9 +478,7 @@ describe("JobsPlugin", () => { const handle = plugin.exports()("etl"); const gen = handle.runAndWait(); - await expect(gen.next()).rejects.toThrow( - "runNow did not return a run_id", - ); + await expect(gen.next()).rejects.toThrow("runNow did not return a runId"); }); }); @@ -544,7 +535,7 @@ describe("JobsPlugin", () => { test("listRuns returns error result on execute failure", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.listRuns.mockImplementation(() => { + jobsApi.listRunsIter.mockImplementation(() => { throw new Error("Auth failure"); }); @@ -587,7 +578,7 @@ describe("JobsPlugin", () => { test("successful operations return ok result with data", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({}); const handle = plugin.exports()("etl"); @@ -595,7 +586,7 @@ describe("JobsPlugin", () => { const result = await handle.runNow(); expect(result.ok).toBe(true); if (result.ok) { - expect(result.data.run_id).toBe(42); + expect(result.data.runId).toBe(42); } }); }); @@ -604,7 +595,7 @@ describe("JobsPlugin", () => { test("getRun returns 404 when run.job_id does not match configured jobId", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.getRun.mockResolvedValue({ run_id: 99, job_id: 456 }); + jobsApi.getRun.mockResolvedValue({ runId: 99n, jobId: 456n }); const plugin = new JobsPlugin({}); const handle = plugin.exports()("etl"); @@ -617,7 +608,7 @@ describe("JobsPlugin", () => { test("getRunOutput returns 404 when run belongs to another job", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.getRun.mockResolvedValue({ run_id: 99, job_id: 456 }); + jobsApi.getRun.mockResolvedValue({ runId: 99n, jobId: 456n }); jobsApi.getRunOutput.mockResolvedValue({ logs: "nope" }); const plugin = new JobsPlugin({}); @@ -633,7 +624,7 @@ describe("JobsPlugin", () => { test("cancelRun returns 404 when run belongs to another job", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.getRun.mockResolvedValue({ run_id: 99, job_id: 456 }); + jobsApi.getRun.mockResolvedValue({ runId: 99n, jobId: 456n }); jobsApi.cancelRun.mockResolvedValue(undefined); const plugin = new JobsPlugin({}); @@ -649,9 +640,9 @@ describe("JobsPlugin", () => { process.env.DATABRICKS_JOB_ETL = "123"; jobsApi.getRun.mockResolvedValue({ - run_id: 42, - job_id: 123, - state: { life_cycle_state: "TERMINATED" }, + runId: 42n, + jobId: 123n, + state: { lifeCycleState: "TERMINATED" }, }); const plugin = new JobsPlugin({}); @@ -659,34 +650,26 @@ describe("JobsPlugin", () => { const result = await handle.getRun(42); expect(result.ok).toBe(true); - if (result.ok) expect(result.data.run_id).toBe(42); + if (result.ok) expect(result.data.runId).toBe(42); }); - test("connector's cancellation token reflects signal state live", async () => { - process.env.DATABRICKS_JOB_ETL = "123"; - - const { Context } = await import("../../../workspace-client"); - const mockContext = Context as unknown as ReturnType; - mockContext.mockClear(); - + test("connector forwards the abort signal as CallOptions", async () => { const { JobsConnector } = await import("../../../connectors/jobs"); const connector = new JobsConnector({}); - jobsApi.get.mockResolvedValue({ job_id: 123 }); + jobsApi.getJob.mockResolvedValue({ jobId: 123n }); const controller = new AbortController(); await connector.getJob( mockClient as never, - { job_id: 123 }, + { jobId: 123n }, controller.signal, ); - const ctorArg = mockContext.mock.calls.at(-1)?.[0] as { - cancellationToken: { isCancellationRequested: boolean }; - }; - expect(ctorArg.cancellationToken.isCancellationRequested).toBe(false); - controller.abort(); - expect(ctorArg.cancellationToken.isCancellationRequested).toBe(true); + expect(jobsApi.getJob).toHaveBeenCalledWith( + { jobId: 123n }, + { signal: controller.signal }, + ); }); }); @@ -694,10 +677,10 @@ describe("JobsPlugin", () => { test("runAndWait stops polling when signal is aborted", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); jobsApi.getRun.mockResolvedValue({ - run_id: 42, - state: { life_cycle_state: "RUNNING" }, + runId: 42n, + state: { lifeCycleState: "RUNNING" }, }); const plugin = new JobsPlugin({ pollIntervalMs: 10 }); @@ -805,14 +788,14 @@ describe("JobsPlugin", () => { process.env.DATABRICKS_JOB_ETL = "100"; process.env.DATABRICKS_JOB_ML = "200"; - jobsApi.runNow.mockResolvedValue({ run_id: 1 }); + jobsApi.runNow.mockResolvedValue({ runId: 1n }); const plugin = new JobsPlugin({}); const exported = plugin.exports(); await exported("etl").runNow(); expect(jobsApi.runNow).toHaveBeenCalledWith( - expect.objectContaining({ job_id: 100 }), + expect.objectContaining({ jobId: 100n }), expect.anything(), ); @@ -820,7 +803,7 @@ describe("JobsPlugin", () => { await exported("ml").runNow(); expect(jobsApi.runNow).toHaveBeenCalledWith( - expect.objectContaining({ job_id: 200 }), + expect.objectContaining({ jobId: 200n }), expect.anything(), ); }); @@ -852,27 +835,27 @@ describe("defaults", () => { describe("mapParams", () => { test("notebook maps to notebook_params with string coercion", () => { const result = mapParams("notebook", { key: "value", num: 42 }); - expect(result).toEqual({ notebook_params: { key: "value", num: "42" } }); + expect(result).toEqual({ notebookParams: { key: "value", num: "42" } }); }); test("python_wheel maps to python_named_params", () => { const result = mapParams("python_wheel", { arg1: "a", arg2: "b" }); - expect(result).toEqual({ python_named_params: { arg1: "a", arg2: "b" } }); + expect(result).toEqual({ pythonNamedParams: { arg1: "a", arg2: "b" } }); }); test("python_script maps to python_params array", () => { const result = mapParams("python_script", { args: ["a", "b", "c"] }); - expect(result).toEqual({ python_params: ["a", "b", "c"] }); + expect(result).toEqual({ pythonParams: ["a", "b", "c"] }); }); test("spark_jar maps to jar_params array", () => { const result = mapParams("spark_jar", { args: ["x", "y"] }); - expect(result).toEqual({ jar_params: ["x", "y"] }); + expect(result).toEqual({ jarParams: ["x", "y"] }); }); test("sql maps to sql_params Record", () => { const result = mapParams("sql", { p1: "v1", p2: 42 }); - expect(result).toEqual({ sql_params: { p1: "v1", p2: "42" } }); + expect(result).toEqual({ sqlParams: { p1: "v1", p2: "42" } }); }); test("dbt with empty params returns empty object", () => { @@ -893,7 +876,7 @@ describe("mapParams", () => { nil: "null", }); expect(result).toEqual({ - notebook_params: { bool: "true", num: "123", nil: "null" }, + notebookParams: { bool: "true", num: "123", nil: "null" }, }); }); }); @@ -1060,7 +1043,7 @@ describe("injectRoutes", () => { test("returns runId on successful non-streaming run", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({}); const routeSpy = vi.spyOn(plugin as any, "route"); @@ -1179,10 +1162,10 @@ describe("injectRoutes", () => { process.env.DATABRICKS_JOB_ETL = "123"; const mockRuns = [ - { run_id: 1, state: { life_cycle_state: "TERMINATED" } }, - { run_id: 2, state: { life_cycle_state: "RUNNING" } }, + { runId: 1n, state: { lifeCycleState: "TERMINATED" } }, + { runId: 2n, state: { lifeCycleState: "RUNNING" } }, ]; - jobsApi.listRuns.mockReturnValue( + jobsApi.listRunsIter.mockReturnValue( (async function* () { for (const run of mockRuns) yield run; })(), @@ -1212,15 +1195,19 @@ describe("injectRoutes", () => { await handler(mockReq, mockRes); + // bigint int64s go out as JSON numbers. expect(mockRes.json).toHaveBeenCalledWith({ - runs: mockRuns, + runs: [ + { runId: 1, state: { lifeCycleState: "TERMINATED" } }, + { runId: 2, state: { lifeCycleState: "RUNNING" } }, + ], }); }); test("passes limit query param to listRuns", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.listRuns.mockReturnValue((async function* () {})()); + jobsApi.listRunsIter.mockReturnValue((async function* () {})()); const plugin = new JobsPlugin({}); const routeSpy = vi.spyOn(plugin as any, "route"); @@ -1247,7 +1234,7 @@ describe("injectRoutes", () => { await handler(mockReq, mockRes); // Verify the connector was called with limit 5 - expect(jobsApi.listRuns).toHaveBeenCalledWith( + expect(jobsApi.listRunsIter).toHaveBeenCalledWith( expect.objectContaining({ limit: 5 }), expect.anything(), ); @@ -1259,9 +1246,9 @@ describe("injectRoutes", () => { process.env.DATABRICKS_JOB_ETL = "123"; const mockRun = { - run_id: 42, - job_id: 123, - state: { life_cycle_state: "TERMINATED" }, + runId: 42n, + jobId: 123n, + state: { lifeCycleState: "TERMINATED" }, }; jobsApi.getRun.mockResolvedValue(mockRun); @@ -1289,7 +1276,11 @@ describe("injectRoutes", () => { await handler(mockReq, mockRes); - expect(mockRes.json).toHaveBeenCalledWith(mockRun); + expect(mockRes.json).toHaveBeenCalledWith({ + runId: 42, + jobId: 123, + state: { lifeCycleState: "TERMINATED" }, + }); }); test("returns 400 for invalid runId", async () => { @@ -1330,7 +1321,7 @@ describe("injectRoutes", () => { process.env.DATABRICKS_JOB_ETL = "123"; // Run exists upstream but is owned by job 456, not the configured 123. - jobsApi.getRun.mockResolvedValue({ run_id: 99, job_id: 456 }); + jobsApi.getRun.mockResolvedValue({ runId: 99n, jobId: 456n }); const plugin = new JobsPlugin({}); const routeSpy = vi.spyOn(plugin as any, "route"); @@ -1359,7 +1350,7 @@ describe("injectRoutes", () => { expect(mockRes.status).toHaveBeenCalledWith(404); // Response body must not leak the run object from the other job. expect(mockRes.json).not.toHaveBeenCalledWith( - expect.objectContaining({ job_id: 456 }), + expect.objectContaining({ jobId: 456 }), ); }); }); @@ -1369,10 +1360,10 @@ describe("injectRoutes", () => { process.env.DATABRICKS_JOB_ETL = "123"; const mockRun = { - run_id: 42, - state: { life_cycle_state: "TERMINATED" }, + runId: 42n, + state: { lifeCycleState: "TERMINATED" }, }; - jobsApi.listRuns.mockReturnValue( + jobsApi.listRunsIter.mockReturnValue( (async function* () { yield mockRun; })(), @@ -1404,14 +1395,14 @@ describe("injectRoutes", () => { expect(mockRes.json).toHaveBeenCalledWith({ status: "TERMINATED", - run: mockRun, + run: { runId: 42, state: { lifeCycleState: "TERMINATED" } }, }); }); test("returns null status when no runs exist", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.listRuns.mockReturnValue((async function* () {})()); + jobsApi.listRunsIter.mockReturnValue((async function* () {})()); const plugin = new JobsPlugin({}); const routeSpy = vi.spyOn(plugin as any, "route"); @@ -1448,7 +1439,7 @@ describe("injectRoutes", () => { test("cancels run and returns 204", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.getRun.mockResolvedValue({ run_id: 42, job_id: 123 }); + jobsApi.getRun.mockResolvedValue({ runId: 42n, jobId: 123n }); jobsApi.cancelRun.mockResolvedValue(undefined); const plugin = new JobsPlugin({}); @@ -1519,7 +1510,7 @@ describe("injectRoutes", () => { process.env.DATABRICKS_JOB_ETL = "123"; // Pre-flight getRun reports a run owned by a different job. - jobsApi.getRun.mockResolvedValue({ run_id: 99, job_id: 456 }); + jobsApi.getRun.mockResolvedValue({ runId: 99n, jobId: 456n }); jobsApi.cancelRun.mockResolvedValue(undefined); const plugin = new JobsPlugin({}); @@ -1701,7 +1692,7 @@ describe("injectRoutes", () => { test("allows exactly MAX_UNVALIDATED_PARAM_KEYS (50) keys without schema", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({ jobs: { etl: { taskType: "notebook" } }, @@ -1745,7 +1736,7 @@ describe("injectRoutes", () => { test("allows undefined params", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.runNow.mockResolvedValue({ run_id: 42 }); + jobsApi.runNow.mockResolvedValue({ runId: 42n }); const plugin = new JobsPlugin({}); const routeSpy = vi.spyOn(plugin as any, "route"); @@ -1826,7 +1817,7 @@ describe("injectRoutes", () => { test("GET /:jobKey/runs returns upstream status on failure", async () => { process.env.DATABRICKS_JOB_ETL = "123"; - jobsApi.listRuns.mockImplementation(() => { + jobsApi.listRunsIter.mockImplementation(() => { throw createApiError({ statusCode: 401, message: "Unauthorized", @@ -1865,7 +1856,7 @@ describe("injectRoutes", () => { process.env.DATABRICKS_JOB_ETL = "123"; // Pre-flight succeeds so we reach the actual cancel call. - jobsApi.getRun.mockResolvedValue({ run_id: 42, job_id: 123 }); + jobsApi.getRun.mockResolvedValue({ runId: 42n, jobId: 123n }); const error = new Error("Forbidden"); (error as any).statusCode = 403; jobsApi.cancelRun.mockRejectedValue(error); diff --git a/packages/appkit/src/plugins/jobs/types.ts b/packages/appkit/src/plugins/jobs/types.ts index af65f80a8..39989cb57 100644 --- a/packages/appkit/src/plugins/jobs/types.ts +++ b/packages/appkit/src/plugins/jobs/types.ts @@ -1,8 +1,14 @@ import type { BasePluginConfig } from "shared"; import type { z } from "zod"; +import type { + JobBaseRun, + JobDetails, + JobRun, + JobRunNowResponse, + JobRunOutput, +} from "../../connectors/jobs"; import type { ExecutionResult } from "../../plugin"; -import type { jobs } from "../../workspace-client"; /** Supported task types for job parameter mapping. */ export type TaskType = @@ -27,7 +33,7 @@ export interface JobConfig { export interface JobRunStatus { status: string | undefined; timestamp: number; - run: jobs.Run; + run: JobRun; } /** User-facing API for a single configured job. */ @@ -35,26 +41,26 @@ export interface JobAPI { /** Trigger the configured job with validated params. Returns the run response. */ runNow( params?: Record, - ): Promise>; + ): Promise>; /** Trigger and poll until completion, yielding status updates. */ runAndWait( params?: Record, signal?: AbortSignal, ): AsyncGenerator; /** Get the most recent run for this job. */ - lastRun(): Promise>; + lastRun(): Promise>; /** List runs for this job. */ listRuns(options?: { limit?: number; - }): Promise>; + }): Promise>; /** Get a specific run by ID. */ - getRun(runId: number): Promise>; + getRun(runId: number): Promise>; /** Get output of a specific run. */ - getRunOutput(runId: number): Promise>; + getRunOutput(runId: number): Promise>; /** Cancel a specific run. */ cancelRun(runId: number): Promise>; /** Get the job definition. */ - getJob(): Promise>; + getJob(): Promise>; } /** Configuration for the Jobs plugin. */ @@ -74,7 +80,7 @@ export interface IJobsConfig extends BasePluginConfig { * @example * ```ts * // Trigger a configured job - * const { run_id } = await appkit.jobs("etl").runNow(); + * const { runId } = await appkit.jobs("etl").runNow(); * * // Trigger and poll until completion * for await (const status of appkit.jobs("etl").runAndWait()) { diff --git a/packages/appkit/src/workspace-client/index.ts b/packages/appkit/src/workspace-client/index.ts index 39e400c6a..d87a83341 100644 --- a/packages/appkit/src/workspace-client/index.ts +++ b/packages/appkit/src/workspace-client/index.ts @@ -6,7 +6,7 @@ */ export { ApiError, ConfigError, Context, createWorkspaceClient } from "shared"; -// Forwards every wrapper type — legacy service namespaces (files/jobs/serving), +// Forwards every wrapper type — legacy service namespaces (files/serving), // the client option/waiter types, and the modular SDK client + model types // (warehouses, statementExecution). `sql` is gone: its statement + warehouse // types now come from the modular SDK. diff --git a/packages/shared/package.json b/packages/shared/package.json index 6002e4693..01938030b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -52,6 +52,7 @@ "@databricks/sdk-core": "0.51.0", "@databricks/sdk-experimental": "0.17.0", "@databricks/sdk-genie": "0.54.0", + "@databricks/sdk-jobs": "0.57.0", "@databricks/sdk-options": "0.51.0", "@databricks/sdk-scim": "0.51.0", "@databricks/sdk-statementexecution": "0.52.0", diff --git a/packages/shared/src/workspace-client/client.ts b/packages/shared/src/workspace-client/client.ts index 1552392e5..92ca56b11 100644 --- a/packages/shared/src/workspace-client/client.ts +++ b/packages/shared/src/workspace-client/client.ts @@ -20,6 +20,8 @@ import { type ScimClient, buildGenieClient, type GenieClient, + buildJobsClient, + type JobsClient, type StatementExecutionClient, type WarehousesClient, type WorkspaceAuth, @@ -35,6 +37,7 @@ export class AppKitWorkspaceClient implements WorkspaceClient { #auth?: WorkspaceAuth; #currentUser?: ScimClient; #genie?: GenieClient; + #jobs?: JobsClient; constructor(opts: WorkspaceClientOptions) { this.#opts = opts; @@ -60,8 +63,12 @@ export class AppKitWorkspaceClient implements WorkspaceClient { return this.#genie; } - get jobs() { - return this.#getLegacy().jobs; + // Migrated to the modular SDK — built lazily, independent of the legacy client. + get jobs(): JobsClient { + if (!this.#jobs) { + this.#jobs = buildJobsClient(this.#opts); + } + return this.#jobs; } // Migrated to the modular SDK — built lazily, independent of the legacy client. diff --git a/packages/shared/src/workspace-client/index.ts b/packages/shared/src/workspace-client/index.ts index f037e8ef4..d55adf9db 100644 --- a/packages/shared/src/workspace-client/index.ts +++ b/packages/shared/src/workspace-client/index.ts @@ -21,6 +21,6 @@ export { Time, TimeUnits, } from "./legacy"; -export type { files, jobs, serving, sql, WorkspaceClient } from "./types"; +export type { files, serving, sql, WorkspaceClient } from "./types"; // Modular SDK client + model types (warehouses). export type * from "./modular"; diff --git a/packages/shared/src/workspace-client/modular.ts b/packages/shared/src/workspace-client/modular.ts index 4e02de2f8..3449d4633 100644 --- a/packages/shared/src/workspace-client/modular.ts +++ b/packages/shared/src/workspace-client/modular.ts @@ -8,9 +8,9 @@ * * Migrated services are built here as per-service clients; the facade delegates * their accessors to these instead of the legacy monolithic client. Currently - * `warehouses` and `statementExecution` are migrated, plus the auth + raw-request - * seam ({@link buildWorkspaceAuth}); every other service still routes through - * `legacy.ts`. + * `warehouses`, `statementExecution`, `currentUser` (SCIM), `genie` and `jobs` + * are migrated, plus the auth + raw-request seam ({@link buildWorkspaceAuth}); + * every other service still routes through `legacy.ts`. * * NOTE: statementExecution relies on a pinned pnpm patch * (`patches/@databricks__sdk-statementexecution@0.46.0.patch`) that restores the @@ -46,6 +46,7 @@ import { } from "@databricks/sdk-core/http"; import { type ProfileOptions, resolve } from "@databricks/sdk-core/profiles"; import type { GenieClient } from "@databricks/sdk-genie/v1"; +import type { JobsClient } from "@databricks/sdk-jobs/v2"; import type { ClientOptions } from "@databricks/sdk-options/client"; import { ScimClient } from "@databricks/sdk-scim/v1"; import { StatementExecutionClient } from "@databricks/sdk-statementexecution/v1"; @@ -544,8 +545,17 @@ export function buildGenieClient(opts: WorkspaceClientOptions): GenieClient { ); } +/** Build a modular Jobs (API 2.2) client from wrapper options. */ +export function buildJobsClient(opts: WorkspaceClientOptions): JobsClient { + return lazyClient( + () => import("@databricks/sdk-jobs/v2").then((m) => m.JobsClient), + mapToClientOptions(opts), + ); +} + // ── Client type re-exports (for the facade accessor types) ─────────────── export type { GenieClient } from "@databricks/sdk-genie/v1"; +export type { JobsClient } from "@databricks/sdk-jobs/v2"; export type { ScimClient } from "@databricks/sdk-scim/v1"; export type { StatementExecutionClient } from "@databricks/sdk-statementexecution/v1"; export type { WarehousesClient } from "@databricks/sdk-warehouses/v1"; @@ -574,6 +584,21 @@ export type { GenieGetMessageQueryResultResponse, GenieMessage, } from "@databricks/sdk-genie/v1"; +export type { + BaseRun, + CancelRunRequest, + GetJobRequest, + GetJobResponse, + GetRunOutputRequest, + GetRunOutputResponse, + GetRunRequest, + GetRunResponse, + ListRunsRequest, + RunNowRequest, + RunNowResponse, + SubmitRunRequest, + SubmitRunResponse, +} from "@databricks/sdk-jobs/v2"; export type { EndpointHealth, EndpointInfo, diff --git a/packages/shared/src/workspace-client/types.ts b/packages/shared/src/workspace-client/types.ts index 7dd98c30f..f347dbc01 100644 --- a/packages/shared/src/workspace-client/types.ts +++ b/packages/shared/src/workspace-client/types.ts @@ -8,7 +8,7 @@ * service means changing its accessor's type here, its getter in `client.ts`, * and the connector that consumes it. * - * Type-namespace re-exports (`files`, `jobs`, `serving`, `sql`) point at + * Type-namespace re-exports (`files`, `serving`, `sql`) point at * `@databricks/sdk-experimental` so existing AppKit call-site shapes stay * stable. They move to the modular `@databricks/sdk-` model exports * as each service migrates. @@ -20,6 +20,7 @@ import type { WorkspaceAuth, ScimClient, GenieClient, + JobsClient, } from "./modular"; // Legacy SDK type namespaces for un-migrated services, re-exported so AppKit @@ -30,8 +31,8 @@ import type { // as `sql.EndpointInfo[]`. It is not on the modular `listWarehouses` because // that request has no `skip_cannot_use` filter, so it could pick a warehouse // the caller can't use. Statement + warehouse service types come from `./modular`. -export type { files, jobs, serving, sql } from "@databricks/sdk-experimental"; -// Modular SDK client + model types (warehouses, statementExecution, genie). +export type { files, serving, sql } from "@databricks/sdk-experimental"; +// Modular SDK client + model types (warehouses, statementExecution, genie, jobs). export type * from "./modular"; /** @@ -52,8 +53,8 @@ export interface WorkspaceClient extends WorkspaceAuth { /** Genie (modular SDK). */ readonly genie: GenieClient; - /** Jobs. */ - readonly jobs: LegacyWorkspaceClient["jobs"]; + /** Jobs (modular SDK, Jobs API 2.2). */ + readonly jobs: JobsClient; /** Statement Execution (modular SDK). */ readonly statementExecution: StatementExecutionClient; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fd69b375f..c1b0f795e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -292,6 +292,9 @@ importers: '@databricks/sdk-genie': specifier: 0.54.0 version: 0.54.0(patch_hash=664f3e13b155eb1b5499486a4f6fc1ab9a6a502e2c6b200d06749bd25dd9ae54) + '@databricks/sdk-jobs': + specifier: 0.57.0 + version: 0.57.0 '@databricks/sdk-options': specifier: 0.51.0 version: 0.51.0 @@ -628,6 +631,9 @@ importers: '@databricks/sdk-genie': specifier: 0.54.0 version: 0.54.0(patch_hash=664f3e13b155eb1b5499486a4f6fc1ab9a6a502e2c6b200d06749bd25dd9ae54) + '@databricks/sdk-jobs': + specifier: 0.57.0 + version: 0.57.0 '@databricks/sdk-options': specifier: 0.51.0 version: 0.51.0 @@ -2025,6 +2031,10 @@ packages: resolution: {integrity: sha512-xT+dyuXocgwPJ5VA1Kr5/dSdKzzR9hRAkEOybeO2i+v891kuuFzVd/M9OM31PtO2/YZp95vdFH5Sb+jVGtm4nw==} engines: {node: '>=22.0.0'} + '@databricks/sdk-jobs@0.57.0': + resolution: {integrity: sha512-LUBLNRhJq0OtUUU6+2T/zRzZMEd4UL0lVm7qTxLIfuDPDNDPy/EHGs3rQ1cXt1Fv0t3Rik7QT/SueZixSsH/Bg==} + engines: {node: '>=22.0.0'} + '@databricks/sdk-options@0.51.0': resolution: {integrity: sha512-p5uBh64Y1onnvwlEwfRsmKmGRI23Y4Ly8fZiCgNUwVDe4Z9d0ctI0mo1v4gnDqlmBaBml3TZfqn5BJBROCXsEw==} engines: {node: '>=22.0.0'} @@ -14346,6 +14356,15 @@ snapshots: json-bigint: 1.0.0 zod: 4.3.6 + '@databricks/sdk-jobs@0.57.0': + dependencies: + '@databricks/sdk-auth': 0.51.0 + '@databricks/sdk-core': 0.51.0 + '@databricks/sdk-options': 0.51.0 + '@js-temporal/polyfill': 0.5.1 + json-bigint: 1.0.0 + zod: 4.3.6 + '@databricks/sdk-options@0.51.0': dependencies: '@databricks/sdk-auth': 0.51.0 diff --git a/template/client/src/pages/jobs/JobsPage.tsx b/template/client/src/pages/jobs/JobsPage.tsx index 7705bedab..32b5b4b1c 100644 --- a/template/client/src/pages/jobs/JobsPage.tsx +++ b/template/client/src/pages/jobs/JobsPage.tsx @@ -11,10 +11,10 @@ import { useCallback, useId, useRef, useState } from 'react'; const MAX_STREAM_LOG = 500; interface Run { - run_id: number; - state?: { life_cycle_state?: string; result_state?: string }; - start_time?: number; - end_time?: number; + runId: number; + state?: { lifeCycleState?: string; resultState?: string }; + startTime?: number; + endTime?: number; } function formatTime(ts?: number) { @@ -205,8 +205,8 @@ export function JobsPage() {

{status.run && (

- Run #{status.run.run_id} — started{' '} - {formatTime(status.run.start_time)} + Run #{status.run.runId} — started{' '} + {formatTime(status.run.startTime)}

)} @@ -232,27 +232,27 @@ export function JobsPage() { {runs.map((run) => ( - - {run.run_id} + + {run.runId} - {run.state?.life_cycle_state ?? '\u2014'} + {run.state?.lifeCycleState ?? '\u2014'} - {run.state?.result_state ?? '\u2014'} + {run.state?.resultState ?? '\u2014'} - {formatTime(run.start_time)} + {formatTime(run.startTime)} - {run.state?.life_cycle_state === 'RUNNING' && ( + {run.state?.lifeCycleState === 'RUNNING' && (