diff --git a/knip.json b/knip.json index 14df12e42..e42dd21c6 100644 --- a/knip.json +++ b/knip.json @@ -13,6 +13,7 @@ "@databricks/sdk-auth", "@databricks/sdk-core", "@databricks/sdk-experimental", + "@databricks/sdk-files", "@databricks/sdk-genie", "@databricks/sdk-jobs", "@databricks/sdk-options", diff --git a/packages/appkit/package.json b/packages/appkit/package.json index d081b45f0..fd0165dfc 100644 --- a/packages/appkit/package.json +++ b/packages/appkit/package.json @@ -74,6 +74,7 @@ "@databricks/sdk-auth": "0.51.0", "@databricks/sdk-core": "0.51.0", "@databricks/sdk-experimental": "0.17.0", + "@databricks/sdk-files": "0.52.0", "@databricks/sdk-genie": "0.54.0", "@databricks/sdk-jobs": "0.57.0", "@databricks/sdk-options": "0.51.0", diff --git a/packages/appkit/src/connectors/files/client.ts b/packages/appkit/src/connectors/files/client.ts index 006fdb63e..2a939d0cd 100644 --- a/packages/appkit/src/connectors/files/client.ts +++ b/packages/appkit/src/connectors/files/client.ts @@ -58,6 +58,47 @@ export function runWithFilesSpanAttributes( return filesSpanAttributesStorage.run(attributes, fn); } +/** + * The modular SDK throws `@databricks/sdk-core`'s `ApiError` (status on + * `httpStatusCode`, code on `.code`), a different class from the wrapper's + * `ApiError`. Convert it so `exists()` and the plugin's `instanceof ApiError` + * / `.statusCode` mapping keep returning upstream 4xx statuses. + */ +function toWrapperApiError(error: unknown): unknown { + if ( + error instanceof Error && + !(error instanceof ApiError) && + "httpStatusCode" in error && + typeof error.httpStatusCode === "number" && + error.httpStatusCode > 0 + ) { + const code = "code" in error ? String(error.code) : "UNKNOWN"; + return new ApiError( + error.message, + code, + error.httpStatusCode, + undefined, + [], + ); + } + return error; +} + +/** Stream a file, mapped back to the legacy `DownloadResponse` shape. */ +async function downloadFile( + client: WorkspaceClient, + resolvedPath: string, +): Promise { + const res = await client.files.downloadFile({ filePath: resolvedPath }); + return { + "content-length": + res.contentLength === undefined ? undefined : Number(res.contentLength), + "content-type": res.contentType, + contents: res.contents, + "last-modified": res.lastModified, + }; +} + interface FilesConnectorConfig { defaultVolume?: string; timeout?: number; @@ -157,7 +198,8 @@ export class FilesConnector { success = true; span.setStatus({ code: SpanStatusCode.OK }); return result; - } catch (error) { + } catch (thrown) { + const error = toWrapperApiError(thrown); span.recordException(error as Error); span.setStatus({ code: SpanStatusCode.ERROR, @@ -192,10 +234,16 @@ export class FilesConnector { return this.traced("list", { "files.path": resolvedPath }, async () => { const entries: DirectoryEntry[] = []; - for await (const entry of client.files.listDirectoryContents({ - directory_path: resolvedPath, + for await (const entry of client.files.listDirectoryContentsIter({ + directoryPath: resolvedPath, })) { - entries.push(entry); + entries.push({ + file_size: entry.fileSize, + is_directory: entry.isDirectory, + last_modified: entry.lastModified, + name: entry.name, + path: entry.path, + }); } return entries; }); @@ -239,11 +287,9 @@ export class FilesConnector { filePath: string, ): Promise { const resolvedPath = this.resolvePath(filePath); - return this.traced("download", { "files.path": resolvedPath }, async () => { - return client.files.download({ - file_path: resolvedPath, - }); - }); + return this.traced("download", { "files.path": resolvedPath }, async () => + downloadFile(client, resolvedPath), + ); } async exists(client: WorkspaceClient, filePath: string): Promise { @@ -267,17 +313,22 @@ export class FilesConnector { ): Promise { const resolvedPath = this.resolvePath(filePath); return this.traced("metadata", { "files.path": resolvedPath }, async () => { - const response = await client.files.getMetadata({ - file_path: resolvedPath, + // The modular `getFileMetadata` parses the empty HEAD body and drops the + // response headers that carry the metadata, so read them off a raw HEAD. + const res = await client.request({ + method: "HEAD", + path: `/api/2.0/fs/files${resolvedPath}`, }); + const contentLength = res.headers.get("content-length"); return { - contentLength: response["content-length"], + contentLength: + contentLength === null ? undefined : Number(contentLength), contentType: contentTypeFromPath( filePath, - response["content-type"], + res.headers.get("content-type") ?? undefined, this.customContentTypes, ), - lastModified: response["last-modified"], + lastModified: res.headers.get("last-modified") ?? undefined, }; }); } @@ -341,9 +392,7 @@ export class FilesConnector { "createDirectory", { "files.path": resolvedPath }, async () => { - await client.files.createDirectory({ - directory_path: resolvedPath, - }); + await client.files.createDirectory({ directoryPath: resolvedPath }); }, ); } @@ -351,9 +400,7 @@ export class FilesConnector { async delete(client: WorkspaceClient, filePath: string): Promise { const resolvedPath = this.resolvePath(filePath); return this.traced("delete", { "files.path": resolvedPath }, async () => { - await client.files.delete({ - file_path: resolvedPath, - }); + await client.files.deleteFile({ filePath: resolvedPath }); }); } @@ -372,9 +419,7 @@ export class FilesConnector { return { ...meta, textPreview: null, isText: false, isImage }; } - const response = await client.files.download({ - file_path: resolvedPath, - }); + const response = await downloadFile(client, resolvedPath); if (!response.contents) { return { ...meta, textPreview: "", isText: true, isImage: false }; } diff --git a/packages/appkit/src/connectors/files/tests/client.test.ts b/packages/appkit/src/connectors/files/tests/client.test.ts index a04a010c4..81cbef230 100644 --- a/packages/appkit/src/connectors/files/tests/client.test.ts +++ b/packages/appkit/src/connectors/files/tests/client.test.ts @@ -5,16 +5,15 @@ import { createApiError } from "../../../testing"; import type { WorkspaceClient } from "../../../workspace-client"; import { ApiError } from "../../../workspace-client"; import { FilesConnector } from "../client"; -import { streamFromChunks, streamFromString } from "./utils"; +import { headResponse, streamFromChunks, streamFromString } from "./utils"; const { mockFilesApi, mockRequest, mockClient } = vi.hoisted(() => { const mockFilesApi = { - listDirectoryContents: vi.fn(), - download: vi.fn(), - getMetadata: vi.fn(), + listDirectoryContentsIter: vi.fn(), + downloadFile: vi.fn(), upload: vi.fn(), createDirectory: vi.fn(), - delete: vi.fn(), + deleteFile: vi.fn(), }; const mockRequest = vi.fn(); @@ -56,11 +55,11 @@ describe("FilesConnector", () => { defaultVolume: "/Volumes/catalog/schema/vol", }); - mockFilesApi.download.mockResolvedValue({ contents: null }); + mockFilesApi.downloadFile.mockResolvedValue({ contents: null }); connector.download(mockClient, "/Volumes/other/path/file.txt"); - expect(mockFilesApi.download).toHaveBeenCalledWith({ - file_path: "/Volumes/other/path/file.txt", + expect(mockFilesApi.downloadFile).toHaveBeenCalledWith({ + filePath: "/Volumes/other/path/file.txt", }); }); @@ -69,11 +68,11 @@ describe("FilesConnector", () => { defaultVolume: "/Volumes/catalog/schema/vol", }); - mockFilesApi.download.mockResolvedValue({ contents: null }); + mockFilesApi.downloadFile.mockResolvedValue({ contents: null }); connector.download(mockClient, "subdir/file.txt"); - expect(mockFilesApi.download).toHaveBeenCalledWith({ - file_path: "/Volumes/catalog/schema/vol/subdir/file.txt", + expect(mockFilesApi.downloadFile).toHaveBeenCalledWith({ + filePath: "/Volumes/catalog/schema/vol/subdir/file.txt", }); }); @@ -155,45 +154,54 @@ describe("FilesConnector", () => { }); }); - test("collects async iterator entries", async () => { - const entries = [ + test("collects async iterator entries, mapped to the snake_case wire shape", async () => { + mockFilesApi.listDirectoryContentsIter.mockReturnValue( + (async function* () { + yield { + name: "file1.txt", + path: "/Volumes/catalog/schema/vol/file1.txt", + isDirectory: false, + fileSize: 42, + lastModified: 1700000000000, + }; + yield { + name: "subdir", + path: "/Volumes/catalog/schema/vol/subdir", + isDirectory: true, + }; + })(), + ); + + const result = await connector.list(mockClient); + + expect(result).toEqual([ { name: "file1.txt", path: "/Volumes/catalog/schema/vol/file1.txt", is_directory: false, + file_size: 42, + last_modified: 1700000000000, }, { name: "subdir", path: "/Volumes/catalog/schema/vol/subdir", is_directory: true, }, - ]; - - mockFilesApi.listDirectoryContents.mockReturnValue( - (async function* () { - for (const entry of entries) { - yield entry; - } - })(), - ); - - const result = await connector.list(mockClient); - - expect(result).toEqual(entries); - expect(mockFilesApi.listDirectoryContents).toHaveBeenCalledWith({ - directory_path: "/Volumes/catalog/schema/vol", + ]); + expect(mockFilesApi.listDirectoryContentsIter).toHaveBeenCalledWith({ + directoryPath: "/Volumes/catalog/schema/vol", }); }); test("uses defaultVolume when no path provided", async () => { - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () {})(), ); await connector.list(mockClient); - expect(mockFilesApi.listDirectoryContents).toHaveBeenCalledWith({ - directory_path: "/Volumes/catalog/schema/vol", + expect(mockFilesApi.listDirectoryContentsIter).toHaveBeenCalledWith({ + directoryPath: "/Volumes/catalog/schema/vol", }); }); @@ -206,31 +214,31 @@ describe("FilesConnector", () => { }); test("uses provided absolute path", async () => { - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () {})(), ); await connector.list(mockClient, "/Volumes/other/path"); - expect(mockFilesApi.listDirectoryContents).toHaveBeenCalledWith({ - directory_path: "/Volumes/other/path", + expect(mockFilesApi.listDirectoryContentsIter).toHaveBeenCalledWith({ + directoryPath: "/Volumes/other/path", }); }); test("resolves relative path with defaultVolume", async () => { - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () {})(), ); await connector.list(mockClient, "subdir"); - expect(mockFilesApi.listDirectoryContents).toHaveBeenCalledWith({ - directory_path: "/Volumes/catalog/schema/vol/subdir", + expect(mockFilesApi.listDirectoryContentsIter).toHaveBeenCalledWith({ + directoryPath: "/Volumes/catalog/schema/vol/subdir", }); }); test("returns empty array for empty directory", async () => { - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () {})(), ); @@ -252,7 +260,7 @@ describe("FilesConnector", () => { test("decodes ReadableStream to UTF-8 string", async () => { const content = "Hello, world!"; - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString(content), }); @@ -262,7 +270,7 @@ describe("FilesConnector", () => { }); test("returns empty string when contents is null", async () => { - mockFilesApi.download.mockResolvedValue({ contents: null }); + mockFilesApi.downloadFile.mockResolvedValue({ contents: null }); const result = await connector.read(mockClient, "empty.txt"); @@ -270,7 +278,7 @@ describe("FilesConnector", () => { }); test("concatenates multiple chunks correctly", async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromChunks(["Hello, ", "world", "!"]), }); @@ -281,7 +289,7 @@ describe("FilesConnector", () => { test("handles multi-byte UTF-8 characters", async () => { const content = "Héllo wârld 🌍"; - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString(content), }); @@ -301,26 +309,55 @@ describe("FilesConnector", () => { }); }); - test("calls client.files.download with resolved path", async () => { - const response = { contents: streamFromString("data") }; - mockFilesApi.download.mockResolvedValue(response); + test("calls client.files.downloadFile with resolved path", async () => { + const contents = streamFromString("data"); + mockFilesApi.downloadFile.mockResolvedValue({ + contentLength: 4n, + contentType: "text/plain", + lastModified: "Mon, 01 Jan 2025 00:00:00 GMT", + contents, + }); const result = await connector.download(mockClient, "file.txt"); - expect(mockFilesApi.download).toHaveBeenCalledWith({ - file_path: "/Volumes/catalog/schema/vol/file.txt", + expect(mockFilesApi.downloadFile).toHaveBeenCalledWith({ + filePath: "/Volumes/catalog/schema/vol/file.txt", + }); + expect(result).toEqual({ + "content-length": 4, + "content-type": "text/plain", + "last-modified": "Mon, 01 Jan 2025 00:00:00 GMT", + contents, }); - expect(result).toBe(response); + expect(result.contents).toBe(contents); + }); + + test("converts a modular-SDK 404 error into the wrapper ApiError", async () => { + mockFilesApi.downloadFile.mockRejectedValue( + Object.assign(new Error("File not found"), { + httpStatusCode: 404, + code: "NOT_FOUND", + }), + ); + + const error = await connector + .download(mockClient, "missing.txt") + .catch((e) => e); + + expect(error).toBeInstanceOf(ApiError); + expect(error.statusCode).toBe(404); + expect(error.errorCode).toBe("NOT_FOUND"); + expect(error.message).toBe("File not found"); }); test("passes absolute path directly", async () => { const response = { contents: null }; - mockFilesApi.download.mockResolvedValue(response); + mockFilesApi.downloadFile.mockResolvedValue(response); await connector.download(mockClient, "/Volumes/other/file.txt"); - expect(mockFilesApi.download).toHaveBeenCalledWith({ - file_path: "/Volumes/other/file.txt", + expect(mockFilesApi.downloadFile).toHaveBeenCalledWith({ + filePath: "/Volumes/other/file.txt", }); }); }); @@ -336,11 +373,13 @@ describe("FilesConnector", () => { }); test("returns true when metadata succeeds", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 100, - "content-type": "text/plain", - "last-modified": "2025-01-01", - }); + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 100, + "content-type": "text/plain", + "last-modified": "2025-01-01", + }), + ); const result = await connector.exists(mockClient, "file.txt"); @@ -348,7 +387,7 @@ describe("FilesConnector", () => { }); test("returns false on 404 ApiError", async () => { - mockFilesApi.getMetadata.mockRejectedValue( + mockRequest.mockRejectedValue( createApiError({ message: "Not found", errorCode: "NOT_FOUND", @@ -362,7 +401,7 @@ describe("FilesConnector", () => { }); test("rethrows non-404 ApiError", async () => { - mockFilesApi.getMetadata.mockRejectedValue( + mockRequest.mockRejectedValue( createApiError({ message: "Server error", errorCode: "SERVER_ERROR", @@ -376,7 +415,7 @@ describe("FilesConnector", () => { }); test("rethrows generic errors", async () => { - mockFilesApi.getMetadata.mockRejectedValue(new Error("Network failure")); + mockRequest.mockRejectedValue(new Error("Network failure")); await expect(connector.exists(mockClient, "file.txt")).rejects.toThrow( "Network failure", @@ -395,11 +434,13 @@ describe("FilesConnector", () => { }); test("maps SDK response to FileMetadata", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 1234, - "content-type": "application/json", - "last-modified": "2025-06-15T10:00:00Z", - }); + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 1234, + "content-type": "application/json", + "last-modified": "2025-06-15T10:00:00Z", + }), + ); const result = await connector.metadata(mockClient, "data.json"); @@ -411,11 +452,13 @@ describe("FilesConnector", () => { }); test("uses contentTypeFromPath to resolve octet-stream", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 500, - "content-type": "application/octet-stream", - "last-modified": "2025-01-01", - }); + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 500, + "content-type": "application/octet-stream", + "last-modified": "2025-01-01", + }), + ); const result = await connector.metadata(mockClient, "image.png"); @@ -423,11 +466,13 @@ describe("FilesConnector", () => { }); test("handles undefined content-type from SDK", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 100, - "content-type": undefined, - "last-modified": "2025-01-01", - }); + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 100, + "content-type": undefined, + "last-modified": "2025-01-01", + }), + ); const result = await connector.metadata(mockClient, "data.csv"); @@ -435,16 +480,41 @@ describe("FilesConnector", () => { }); test("resolves relative path via defaultVolume", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 0, - "content-type": "text/plain", - "last-modified": "2025-01-01", - }); + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 0, + "content-type": "text/plain", + "last-modified": "2025-01-01", + }), + ); await connector.metadata(mockClient, "notes.txt"); - expect(mockFilesApi.getMetadata).toHaveBeenCalledWith({ - file_path: "/Volumes/catalog/schema/vol/notes.txt", + expect(mockRequest).toHaveBeenCalledWith({ + method: "HEAD", + path: "/api/2.0/fs/files/Volumes/catalog/schema/vol/notes.txt", + }); + }); + + // Regression: the modular SDK's getFileMetadata drops HEAD response + // headers, so metadata must come from the raw HEAD response headers. + test("reads metadata from the HEAD response headers", async () => { + mockRequest.mockResolvedValue( + new Response(null, { + headers: { + "content-length": "2048", + "content-type": "text/plain", + "last-modified": "Sun, 15 Jun 2025 10:00:00 GMT", + }, + }), + ); + + const result = await connector.metadata(mockClient, "notes.txt"); + + expect(result).toEqual({ + contentLength: 2048, + contentType: "text/plain", + lastModified: "Sun, 15 Jun 2025 10:00:00 GMT", }); }); }); @@ -555,7 +625,7 @@ describe("FilesConnector", () => { await connector.createDirectory(mockClient, "new-dir"); expect(mockFilesApi.createDirectory).toHaveBeenCalledWith({ - directory_path: "/Volumes/catalog/schema/vol/new-dir", + directoryPath: "/Volumes/catalog/schema/vol/new-dir", }); }); @@ -568,7 +638,7 @@ describe("FilesConnector", () => { ); expect(mockFilesApi.createDirectory).toHaveBeenCalledWith({ - directory_path: "/Volumes/other/path/new-dir", + directoryPath: "/Volumes/other/path/new-dir", }); }); }); @@ -583,23 +653,23 @@ describe("FilesConnector", () => { }); }); - test("calls client.files.delete with resolved path", async () => { - mockFilesApi.delete.mockResolvedValue(undefined); + test("calls client.files.deleteFile with resolved path", async () => { + mockFilesApi.deleteFile.mockResolvedValue(undefined); await connector.delete(mockClient, "file.txt"); - expect(mockFilesApi.delete).toHaveBeenCalledWith({ - file_path: "/Volumes/catalog/schema/vol/file.txt", + expect(mockFilesApi.deleteFile).toHaveBeenCalledWith({ + filePath: "/Volumes/catalog/schema/vol/file.txt", }); }); test("uses absolute path when provided", async () => { - mockFilesApi.delete.mockResolvedValue(undefined); + mockFilesApi.deleteFile.mockResolvedValue(undefined); await connector.delete(mockClient, "/Volumes/other/file.txt"); - expect(mockFilesApi.delete).toHaveBeenCalledWith({ - file_path: "/Volumes/other/file.txt", + expect(mockFilesApi.deleteFile).toHaveBeenCalledWith({ + filePath: "/Volumes/other/file.txt", }); }); }); @@ -617,12 +687,14 @@ describe("FilesConnector", () => { test("text files return truncated preview (max 1024 chars)", async () => { const longText = "A".repeat(2000); - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 2000, - "content-type": "text/plain", - "last-modified": "2025-01-01", - }); - mockFilesApi.download.mockResolvedValue({ + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 2000, + "content-type": "text/plain", + "last-modified": "2025-01-01", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString(longText), }); @@ -635,12 +707,14 @@ describe("FilesConnector", () => { }); test("text/html files are treated as text", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 30, - "content-type": "text/html", - "last-modified": "2025-01-01", - }); - mockFilesApi.download.mockResolvedValue({ + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 30, + "content-type": "text/html", + "last-modified": "2025-01-01", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString("

Hello

"), }); @@ -651,12 +725,14 @@ describe("FilesConnector", () => { }); test("application/json files are treated as text", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 20, - "content-type": "application/json", - "last-modified": "2025-01-01", - }); - mockFilesApi.download.mockResolvedValue({ + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 20, + "content-type": "application/json", + "last-modified": "2025-01-01", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString('{"key":"value"}'), }); @@ -667,12 +743,14 @@ describe("FilesConnector", () => { }); test("application/xml files are treated as text", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 30, - "content-type": "application/xml", - "last-modified": "2025-01-01", - }); - mockFilesApi.download.mockResolvedValue({ + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 30, + "content-type": "application/xml", + "last-modified": "2025-01-01", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString(""), }); @@ -683,11 +761,13 @@ describe("FilesConnector", () => { }); test("image files return isImage: true, textPreview: null", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 5000, - "content-type": "image/png", - "last-modified": "2025-01-01", - }); + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 5000, + "content-type": "image/png", + "last-modified": "2025-01-01", + }), + ); const result = await connector.preview(mockClient, "image.png"); @@ -697,11 +777,13 @@ describe("FilesConnector", () => { }); test("other files return isText: false, isImage: false, textPreview: null", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 1000, - "content-type": "application/pdf", - "last-modified": "2025-01-01", - }); + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 1000, + "content-type": "application/pdf", + "last-modified": "2025-01-01", + }), + ); const result = await connector.preview(mockClient, "doc.pdf"); @@ -711,12 +793,14 @@ describe("FilesConnector", () => { }); test("empty file contents return empty string preview", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 0, - "content-type": "text/plain", - "last-modified": "2025-01-01", - }); - mockFilesApi.download.mockResolvedValue({ + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 0, + "content-type": "text/plain", + "last-modified": "2025-01-01", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: null, }); @@ -728,12 +812,14 @@ describe("FilesConnector", () => { }); test("preview spreads metadata into result", async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 42, - "content-type": "text/plain", - "last-modified": "2025-06-15T10:00:00Z", - }); - mockFilesApi.download.mockResolvedValue({ + mockRequest.mockResolvedValue( + headResponse({ + "content-length": 42, + "content-type": "text/plain", + "last-modified": "2025-06-15T10:00:00Z", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString("hello"), }); @@ -747,12 +833,14 @@ describe("FilesConnector", () => { test("short text file returns full content", async () => { const content = "Short file."; - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": content.length, - "content-type": "text/plain", - "last-modified": "2025-01-01", - }); - mockFilesApi.download.mockResolvedValue({ + mockRequest.mockResolvedValue( + headResponse({ + "content-length": content.length, + "content-type": "text/plain", + "last-modified": "2025-01-01", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString(content), }); diff --git a/packages/appkit/src/connectors/files/tests/utils.ts b/packages/appkit/src/connectors/files/tests/utils.ts index d204d8dad..47a2c1158 100644 --- a/packages/appkit/src/connectors/files/tests/utils.ts +++ b/packages/appkit/src/connectors/files/tests/utils.ts @@ -20,3 +20,14 @@ export function streamFromChunks(chunks: string[]): ReadableStream { }, }); } + +// Builds the HEAD response the connector's metadata() reads headers from. +export function headResponse( + headers: Record, +): Response { + const h = new Headers(); + for (const [key, value] of Object.entries(headers)) { + if (value !== undefined) h.set(key, String(value)); + } + return new Response(null, { headers: h }); +} diff --git a/packages/appkit/src/plugins/files/tests/delete.test.ts b/packages/appkit/src/plugins/files/tests/delete.test.ts index 9ef00f942..a87c45987 100644 --- a/packages/appkit/src/plugins/files/tests/delete.test.ts +++ b/packages/appkit/src/plugins/files/tests/delete.test.ts @@ -29,12 +29,11 @@ describe("FilesPlugin delete", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); @@ -52,7 +51,7 @@ describe("FilesPlugin delete", () => { const generateKey = vi.spyOn(testCache.current, "generateKey"); const del = vi.spyOn(testCache.current, "delete"); - getMock(client, "files.delete").mockResolvedValue(undefined); + getMock(client, "files.deleteFile").mockResolvedValue(undefined); await handler( mockReq("uploads", { @@ -86,7 +85,7 @@ describe("FilesPlugin delete", () => { const handler = getRouteHandler(plugin, "delete", ""); const res = mockRes(); - getMock(client, "files.delete").mockRejectedValue( + getMock(client, "files.deleteFile").mockRejectedValue( createApiError({ statusCode: 404, message: "Not found", diff --git a/packages/appkit/src/plugins/files/tests/download-endpoint.test.ts b/packages/appkit/src/plugins/files/tests/download-endpoint.test.ts index 622aa702d..f166bface 100644 --- a/packages/appkit/src/plugins/files/tests/download-endpoint.test.ts +++ b/packages/appkit/src/plugins/files/tests/download-endpoint.test.ts @@ -28,12 +28,11 @@ describe("FilesPlugin download endpoint Content-Disposition", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); @@ -48,7 +47,7 @@ describe("FilesPlugin download endpoint Content-Disposition", () => { const handler = getRouteHandler(plugin, "get", "/download"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse("file data"), ); @@ -70,7 +69,7 @@ describe("FilesPlugin download endpoint Content-Disposition", () => { const handler = getRouteHandler(plugin, "get", "/download"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse("data"), ); @@ -92,7 +91,7 @@ describe("FilesPlugin download endpoint Content-Disposition", () => { const handler = getRouteHandler(plugin, "get", "/download"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse("{}"), ); @@ -132,7 +131,7 @@ describe("FilesPlugin download endpoint Content-Disposition", () => { const res = mockRes(); // Response with no contents field (empty file) - getMock(client, "files.download").mockResolvedValue({}); + getMock(client, "files.downloadFile").mockResolvedValue({}); await handler( mockReq("uploads", { diff --git a/packages/appkit/src/plugins/files/tests/error-handling.test.ts b/packages/appkit/src/plugins/files/tests/error-handling.test.ts index 296b6f2b8..1c5652327 100644 --- a/packages/appkit/src/plugins/files/tests/error-handling.test.ts +++ b/packages/appkit/src/plugins/files/tests/error-handling.test.ts @@ -28,12 +28,11 @@ describe("FilesPlugin error handling", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); diff --git a/packages/appkit/src/plugins/files/tests/path-validation.test.ts b/packages/appkit/src/plugins/files/tests/path-validation.test.ts index 42e21b883..c5dd40a79 100644 --- a/packages/appkit/src/plugins/files/tests/path-validation.test.ts +++ b/packages/appkit/src/plugins/files/tests/path-validation.test.ts @@ -28,12 +28,11 @@ describe("FilesPlugin path validation", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); @@ -46,13 +45,13 @@ describe("FilesPlugin path validation", () => { // Defends against regressions where the handler calls the SDK and *also* // returns 400 β€” the status assertion alone wouldn't catch that. function expectNoSdkCall() { - expect(getMock(client, "files.download")).not.toHaveBeenCalled(); + expect(getMock(client, "files.downloadFile")).not.toHaveBeenCalled(); expect(getMock(client, "files.upload")).not.toHaveBeenCalled(); - expect(getMock(client, "files.delete")).not.toHaveBeenCalled(); + expect(getMock(client, "files.deleteFile")).not.toHaveBeenCalled(); expect(getMock(client, "files.createDirectory")).not.toHaveBeenCalled(); - expect(getMock(client, "files.getMetadata")).not.toHaveBeenCalled(); + expect(getMock(client, "request")).not.toHaveBeenCalled(); expect( - getMock(client, "files.listDirectoryContents"), + getMock(client, "files.listDirectoryContentsIter"), ).not.toHaveBeenCalled(); } diff --git a/packages/appkit/src/plugins/files/tests/plugin.integration.test.ts b/packages/appkit/src/plugins/files/tests/plugin.integration.test.ts index 2fd955297..b60fdbfc7 100644 --- a/packages/appkit/src/plugins/files/tests/plugin.integration.test.ts +++ b/packages/appkit/src/plugins/files/tests/plugin.integration.test.ts @@ -20,20 +20,20 @@ import { createApp } from "../../../core"; import { createApiError } from "../../../testing"; import { server as serverPlugin } from "../../server"; import { files } from "../index"; -import { streamFromString } from "./utils"; +import { headResponse, streamFromString } from "./utils"; const { mockFilesApi, mockSdkClient } = vi.hoisted(() => { const mockFilesApi = { - listDirectoryContents: vi.fn(), - download: vi.fn(), - getMetadata: vi.fn(), + listDirectoryContentsIter: vi.fn(), + downloadFile: vi.fn(), upload: vi.fn(), createDirectory: vi.fn(), - delete: vi.fn(), + deleteFile: vi.fn(), }; const mockSdkClient = { files: mockFilesApi, + request: vi.fn(), config: { host: "https://test.databricks.com", authenticate: vi.fn(), @@ -101,12 +101,12 @@ describe("Files Plugin Integration", () => { }); beforeEach(() => { - mockFilesApi.listDirectoryContents.mockReset(); - mockFilesApi.download.mockReset(); - mockFilesApi.getMetadata.mockReset(); + mockFilesApi.listDirectoryContentsIter.mockReset(); + mockFilesApi.downloadFile.mockReset(); + mockSdkClient.request.mockReset().mockResolvedValue(new Response(null)); mockFilesApi.upload.mockReset(); mockFilesApi.createDirectory.mockReset(); - mockFilesApi.delete.mockReset(); + mockFilesApi.deleteFile.mockReset(); }); describe("Volumes Endpoint", () => { @@ -148,10 +148,11 @@ describe("Files Plugin Integration", () => { }, ]; - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () { - for (const entry of MOCKED_ENTRIES) { - yield entry; + // Modular SDK yields camelCase; the HTTP body stays snake_case. + for (const { is_directory, ...rest } of MOCKED_ENTRIES) { + yield { ...rest, isDirectory: is_directory }; } })(), ); @@ -166,7 +167,7 @@ describe("Files Plugin Integration", () => { }); test(`GET /api/files/${VOL}/list?path=/abs/path uses provided path`, async () => { - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () {})(), ); @@ -176,15 +177,15 @@ describe("Files Plugin Integration", () => { ); expect(response.status).toBe(200); - expect(mockFilesApi.listDirectoryContents).toHaveBeenCalledWith({ - directory_path: "/Volumes/other/path", + expect(mockFilesApi.listDirectoryContentsIter).toHaveBeenCalledWith({ + directoryPath: "/Volumes/other/path", }); }); }); describe("Read File", () => { test(`GET /api/files/${VOL}/read?path=/file.txt returns text content`, async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString("file content here"), }); @@ -211,11 +212,13 @@ describe("Files Plugin Integration", () => { describe("Exists", () => { test(`GET /api/files/${VOL}/exists returns { exists: true }`, async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 100, - "content-type": "text/plain", - "last-modified": "2025-01-01", - }); + mockSdkClient.request.mockResolvedValue( + headResponse({ + "content-length": 100, + "content-type": "text/plain", + "last-modified": "2025-01-01", + }), + ); const response = await fetch( `${baseUrl}/api/files/${VOL}/exists?path=/Volumes/catalog/schema/vol/file.txt`, @@ -228,7 +231,7 @@ describe("Files Plugin Integration", () => { }); test(`GET /api/files/${VOL}/exists returns { exists: false } on 404`, async () => { - mockFilesApi.getMetadata.mockRejectedValue( + mockSdkClient.request.mockRejectedValue( createApiError({ statusCode: 404, message: "Not found", @@ -249,11 +252,13 @@ describe("Files Plugin Integration", () => { describe("Metadata", () => { test(`GET /api/files/${VOL}/metadata returns correct metadata`, async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 256, - "content-type": "application/json", - "last-modified": "2025-06-15T10:00:00Z", - }); + mockSdkClient.request.mockResolvedValue( + headResponse({ + "content-length": 256, + "content-type": "application/json", + "last-modified": "2025-06-15T10:00:00Z", + }), + ); const response = await fetch( `${baseUrl}/api/files/${VOL}/metadata?path=/Volumes/catalog/schema/vol/file.json`, @@ -272,12 +277,14 @@ describe("Files Plugin Integration", () => { describe("Preview", () => { test(`GET /api/files/${VOL}/preview returns text preview`, async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 20, - "content-type": "text/plain", - "last-modified": "2025-01-01", - }); - mockFilesApi.download.mockResolvedValue({ + mockSdkClient.request.mockResolvedValue( + headResponse({ + "content-length": 20, + "content-type": "text/plain", + "last-modified": "2025-01-01", + }), + ); + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString("Hello preview!"), }); @@ -298,11 +305,13 @@ describe("Files Plugin Integration", () => { }); test(`GET /api/files/${VOL}/preview returns image metadata`, async () => { - mockFilesApi.getMetadata.mockResolvedValue({ - "content-length": 5000, - "content-type": "image/png", - "last-modified": "2025-01-01", - }); + mockSdkClient.request.mockResolvedValue( + headResponse({ + "content-length": 5000, + "content-type": "image/png", + "last-modified": "2025-01-01", + }), + ); const response = await fetch( `${baseUrl}/api/files/${VOL}/preview?path=/Volumes/catalog/schema/vol/image.png`, @@ -323,7 +332,7 @@ describe("Files Plugin Integration", () => { describe("Raw Endpoint Security Headers", () => { test("safe type (image/png) sets security headers without Content-Disposition", async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString("PNG data"), }); @@ -340,7 +349,7 @@ describe("Files Plugin Integration", () => { }); test("dangerous type (text/html) forces download via Content-Disposition", async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString(""), }); @@ -359,7 +368,7 @@ describe("Files Plugin Integration", () => { }); test("SVG (image/svg+xml) is treated as dangerous", async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString(""), }); @@ -377,7 +386,7 @@ describe("Files Plugin Integration", () => { }); test("JavaScript (text/javascript) is treated as dangerous", async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString("alert('xss')"), }); @@ -395,7 +404,7 @@ describe("Files Plugin Integration", () => { }); test("safe type (application/json) is served inline", async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString('{"key":"value"}'), }); @@ -414,7 +423,7 @@ describe("Files Plugin Integration", () => { describe("Download Endpoint Security Headers", () => { test("sets X-Content-Type-Options: nosniff", async () => { - mockFilesApi.download.mockResolvedValue({ + mockFilesApi.downloadFile.mockResolvedValue({ contents: streamFromString("file data"), }); @@ -433,7 +442,7 @@ describe("Files Plugin Integration", () => { describe("Service principal execution", () => { test("header-less request + default publicRead() + list β†’ 200 (policy decides)", async () => { - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () { yield { name: "sp-file.txt", @@ -523,7 +532,7 @@ describe("Files Plugin Integration", () => { const port = await getListeningPort(appkit.server.getServer()); const localBase = `http://127.0.0.1:${port}`; - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () { yield { name: "spy-file.txt", @@ -552,7 +561,7 @@ describe("Files Plugin Integration", () => { }); test("requests with user headers also succeed", async () => { - mockFilesApi.listDirectoryContents.mockReturnValue( + mockFilesApi.listDirectoryContentsIter.mockReturnValue( (async function* () { yield { name: "file.txt", @@ -631,7 +640,7 @@ describe("Files Plugin Integration", () => { describe("Error Handling", () => { test("SDK exceptions return 500 with generic error", async () => { - mockFilesApi.getMetadata.mockRejectedValue( + mockSdkClient.request.mockRejectedValue( new Error("SDK connection failed"), ); @@ -647,7 +656,7 @@ describe("Files Plugin Integration", () => { }); test("list errors return 500", async () => { - mockFilesApi.listDirectoryContents.mockRejectedValue( + mockFilesApi.listDirectoryContentsIter.mockRejectedValue( new Error("Permission denied"), ); @@ -663,7 +672,7 @@ describe("Files Plugin Integration", () => { }); test("ApiError 404 preserves upstream status code", async () => { - mockFilesApi.getMetadata.mockRejectedValue( + mockSdkClient.request.mockRejectedValue( createApiError({ statusCode: 404, message: "Not found", @@ -686,7 +695,7 @@ describe("Files Plugin Integration", () => { }); test("ApiError 409 preserves upstream status code", async () => { - mockFilesApi.getMetadata.mockRejectedValue( + mockSdkClient.request.mockRejectedValue( createApiError({ statusCode: 409, message: "Conflict", diff --git a/packages/appkit/src/plugins/files/tests/plugin.test.ts b/packages/appkit/src/plugins/files/tests/plugin.test.ts index a0a3b84b1..877db13c2 100644 --- a/packages/appkit/src/plugins/files/tests/plugin.test.ts +++ b/packages/appkit/src/plugins/files/tests/plugin.test.ts @@ -21,12 +21,11 @@ import { PolicyDeniedError, policy, type FilePolicyUser } from "../policy"; const { mockClient, MockApiError } = await vi.hoisted(async () => { const mockFilesApi = { - listDirectoryContents: vi.fn(), - download: vi.fn(), - getMetadata: vi.fn(), + listDirectoryContentsIter: vi.fn(), + downloadFile: vi.fn(), upload: vi.fn(), createDirectory: vi.fn(), - delete: vi.fn(), + deleteFile: vi.fn(), }; const mockClient = { @@ -224,10 +223,10 @@ describe("FilesPlugin", () => { for (const item of items) yield item; }, }); - mockClient.files.listDirectoryContents.mockReturnValueOnce( + mockClient.files.listDirectoryContentsIter.mockReturnValueOnce( asyncIterable([{ path: "uploads-file" }]), ); - mockClient.files.listDirectoryContents.mockReturnValueOnce( + mockClient.files.listDirectoryContentsIter.mockReturnValueOnce( asyncIterable([{ path: "exports-file" }]), ); @@ -697,7 +696,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandlerForTimeout(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "file.txt", path: "/file.txt", is_directory: false }; }, @@ -723,7 +722,7 @@ describe("FilesPlugin", () => { // Simulate an SDK call that rejects (e.g. network error). // Returns an async iterable whose first iteration throws. - mockClient.files.listDirectoryContents.mockReturnValue({ + mockClient.files.listDirectoryContentsIter.mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(new Error("network failure")), }), @@ -748,7 +747,9 @@ describe("FilesPlugin", () => { const handler = getRouteHandlerForTimeout(plugin, "get", "/read"); const res = mockRes(); - mockClient.files.download.mockRejectedValue(new Error("network failure")); + mockClient.files.downloadFile.mockRejectedValue( + new Error("network failure"), + ); const handlerPromise = handler( mockReq("uploads", { query: { path: "test.txt" } }), @@ -770,9 +771,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandlerForTimeout(plugin, "get", "/exists"); const res = mockRes(); - mockClient.files.getMetadata.mockRejectedValue( - new Error("network failure"), - ); + mockClient.request.mockRejectedValue(new Error("network failure")); const handlerPromise = handler( mockReq("uploads", { query: { path: "test.txt" } }), @@ -794,9 +793,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandlerForTimeout(plugin, "get", "/metadata"); const res = mockRes(); - mockClient.files.getMetadata.mockRejectedValue( - new Error("network failure"), - ); + mockClient.request.mockRejectedValue(new Error("network failure")); const handlerPromise = handler( mockReq("uploads", { query: { path: "test.txt" } }), @@ -818,7 +815,9 @@ describe("FilesPlugin", () => { const handler = getRouteHandlerForTimeout(plugin, "get", "/download"); const res = mockRes(); - mockClient.files.download.mockRejectedValue(new Error("network failure")); + mockClient.files.downloadFile.mockRejectedValue( + new Error("network failure"), + ); const handlerPromise = handler( mockReq("uploads", { query: { path: "big.bin" } }), @@ -885,7 +884,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandlerForTimeout(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockReturnValue({ + mockClient.files.listDirectoryContentsIter.mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: () => Promise.reject(new Error("internal: secret connection string xyz")), @@ -915,7 +914,7 @@ describe("FilesPlugin", () => { let signalWasAborted = false; const { promise, capturedReject } = hangingWithAbort(); - mockClient.files.listDirectoryContents.mockReturnValue({ + mockClient.files.listDirectoryContentsIter.mockReturnValue({ [Symbol.asyncIterator]: () => ({ next: () => { // Simulate: we set up a timeout that rejects the hanging promise, @@ -1047,7 +1046,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "h.txt", path: "/h.txt", is_directory: false }; }, @@ -1162,7 +1161,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "g.txt", path: "/g.txt", is_directory: false }; }, @@ -1250,7 +1249,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "a.txt", path: "/a.txt", is_directory: false }; }, @@ -1286,7 +1285,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "b.txt", path: "/b.txt", is_directory: false }; }, @@ -1307,7 +1306,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "c.txt", path: "/c.txt", is_directory: false }; }, @@ -1449,7 +1448,7 @@ describe("FilesPlugin", () => { const exported = plugin.exports(); const handle = exported("public"); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "d.txt", path: "/d.txt", is_directory: false }; }, @@ -1626,7 +1625,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/read"); const res = mockRes(); - mockClient.files.download.mockResolvedValue({ + mockClient.files.downloadFile.mockResolvedValue({ contents: new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("file content")); @@ -2058,7 +2057,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "o.txt", path: "/o.txt", is_directory: false }; }, @@ -2119,7 +2118,7 @@ describe("FilesPlugin", () => { // Policy must not have been evaluated and the SDK must not have been // called. expect(policySpy).not.toHaveBeenCalled(); - expect(mockClient.files.listDirectoryContents).not.toHaveBeenCalled(); + expect(mockClient.files.listDirectoryContentsIter).not.toHaveBeenCalled(); }); test("OBO volume + missing token + NODE_ENV === 'development' β†’ exactly one warn, SP fallback proceeds", async () => { @@ -2135,7 +2134,7 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/list"); const res = mockRes(); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "d.txt", path: "/d.txt", is_directory: false }; }, @@ -2319,7 +2318,7 @@ describe("FilesPlugin", () => { // assert that the SDK call ran inside `runInUserContext` with the // expected user identity. const observedUserIds: string[] = []; - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { // getCurrentPrincipalId() inside the wrapped fn should resolve to alice. const ctx = await import("../../../context"); @@ -2358,9 +2357,9 @@ describe("FilesPlugin", () => { const handler = getRouteHandler(plugin, "get", "/read"); const res = mockRes(); - // The connector reads via files.download β€” return a valid 200-ish + // The connector reads via files.downloadFile β€” return a valid 200-ish // response with content body. - mockClient.files.download.mockImplementation(async () => ({ + mockClient.files.downloadFile.mockImplementation(async () => ({ contents: new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode("hello")); @@ -2423,7 +2422,7 @@ describe("FilesPlugin", () => { }); const handler = getRouteHandler(plugin, "get", "/list"); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -2452,7 +2451,9 @@ describe("FilesPlugin", () => { // Cache is disabled on OBO: `getOrExecute` is bypassed. The SDK // must execute on every request β€” no cross-user staleness possible. expect(getOrExecute).not.toHaveBeenCalled(); - expect(mockClient.files.listDirectoryContents).toHaveBeenCalledTimes(2); + expect(mockClient.files.listDirectoryContentsIter).toHaveBeenCalledTimes( + 2, + ); }); test("SP volume reads still use the cache (cache is only disabled for OBO)", async () => { @@ -2471,7 +2472,7 @@ describe("FilesPlugin", () => { const listHandler = getRouteHandler(plugin, "get", "/list"); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -2532,7 +2533,7 @@ describe("FilesPlugin", () => { }); const handler = getRouteHandler(plugin, "get", "/list"); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -2849,7 +2850,7 @@ describe("FilesPlugin", () => { await useRealGetCurrentPrincipalId(); await useRealGetWorkspaceClient(); - // Distinct user-token client with a `files.delete` that mimics a UC + // Distinct user-token client with a `files.deleteFile` that mimics a UC // failure (e.g. 403 from UC because the user lacks privilege). const userClient = { config: { @@ -2859,7 +2860,7 @@ describe("FilesPlugin", () => { }), }, files: { - delete: vi.fn(async () => { + deleteFile: vi.fn(async () => { throw new MockApiError("UC denied", 403); }), }, @@ -2899,8 +2900,8 @@ describe("FilesPlugin", () => { ); // The user-token client was used, not the SP one. - expect(userClient.files.delete).toHaveBeenCalledTimes(1); - expect(mockClient.files.delete).not.toHaveBeenCalled(); + expect(userClient.files.deleteFile).toHaveBeenCalledTimes(1); + expect(mockClient.files.deleteFile).not.toHaveBeenCalled(); // The UC error surfaced as 403 (the ApiError statusCode). expect(res.status).toHaveBeenCalledWith(403); @@ -3161,7 +3162,7 @@ describe("FilesPlugin", () => { // [{...}]. We toggle the return AFTER alice's upload has reached // the SDK to simulate cross-user freshness. let postUploadVisible = false; - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { if (postUploadVisible) { yield { @@ -3275,13 +3276,13 @@ describe("FilesPlugin", () => { // `.files` β€” making any leak through the SP path crash loudly). await useRealGetWorkspaceClient(); - // Distinct user-token client whose `listDirectoryContents` is the + // Distinct user-token client whose `listDirectoryContentsIter` is the // one we expect `asUser` to route through. const userListSpy = vi.fn(async function* () { yield { name: "user.txt", path: "/user.txt", is_directory: false }; }); const userClient = { - files: { listDirectoryContents: userListSpy }, + files: { listDirectoryContentsIter: userListSpy }, }; // Wire `_buildUserContextOrNull β†’ ServiceContext.createUserContext` to @@ -3347,7 +3348,7 @@ describe("FilesPlugin", () => { const spListSpy = vi.fn(async function* () { yield { name: "sp.txt", path: "/sp.txt", is_directory: false }; }); - mockClient.files.listDirectoryContents.mockImplementation(spListSpy); + mockClient.files.listDirectoryContentsIter.mockImplementation(spListSpy); // Spy on createUserContext to confirm no wrap happened. serviceContextMock.createUserContextSpy.mockClear(); @@ -3384,7 +3385,7 @@ describe("FilesPlugin", () => { const spListSpy = vi.fn(async function* () { yield { name: "sp.txt", path: "/sp.txt", is_directory: false }; }); - mockClient.files.listDirectoryContents.mockImplementation(spListSpy); + mockClient.files.listDirectoryContentsIter.mockImplementation(spListSpy); serviceContextMock.createUserContextSpy.mockClear(); @@ -3439,12 +3440,12 @@ describe("FilesPlugin", () => { process.env.NODE_ENV = "development"; const policySpy = vi.fn().mockReturnValue(true); - // Use the default mocked SP client; spy on its listDirectoryContents + // Use the default mocked SP client; spy on its listDirectoryContentsIter // to confirm the SDK call resolved against the SP path. const spListSpy = vi.fn(async function* () { yield { name: "sp.txt", path: "/sp.txt", is_directory: false }; }); - mockClient.files.listDirectoryContents.mockImplementation(spListSpy); + mockClient.files.listDirectoryContentsIter.mockImplementation(spListSpy); serviceContextMock.createUserContextSpy.mockClear(); @@ -3596,7 +3597,7 @@ describe("FilesPlugin", () => { }); const calls = spyOnTelemetry(plugin); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -3622,7 +3623,7 @@ describe("FilesPlugin", () => { const plugin = new FilesPlugin(VOLUMES_CONFIG); const calls = spyOnTelemetry(plugin); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -3648,7 +3649,7 @@ describe("FilesPlugin", () => { const plugin = new FilesPlugin(VOLUMES_CONFIG); const calls = spyOnTelemetry(plugin); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -3685,7 +3686,7 @@ describe("FilesPlugin", () => { const plugin = new FilesPlugin(VOLUMES_CONFIG); const calls = spyOnTelemetry(plugin); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -3714,7 +3715,7 @@ describe("FilesPlugin", () => { const plugin = new FilesPlugin(VOLUMES_CONFIG); const calls = spyOnTelemetry(plugin); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -3747,7 +3748,7 @@ describe("FilesPlugin", () => { }); const calls = spyOnTelemetry(plugin); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () { yield { name: "f.txt", path: "/f.txt", is_directory: false }; }, @@ -3805,7 +3806,7 @@ describe("FilesPlugin", () => { const plugin = new FilesPlugin({ volumes: { uploads: { policy: recorder }, exports: {} }, }); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () {}, ); const handle = plugin.exports()("uploads"); @@ -3842,7 +3843,7 @@ describe("FilesPlugin", () => { const plugin = new FilesPlugin({ volumes: { uploads: { policy: spOnly }, exports: {} }, }); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () {}, ); const handle = plugin.exports()("uploads"); @@ -3856,7 +3857,7 @@ describe("FilesPlugin", () => { test("appkit.asUser(req) tags files.auth_mode as on-behalf-of-user", async () => { const plugin = new FilesPlugin(VOLUMES_CONFIG); const calls = spyOnTelemetry(plugin); - mockClient.files.listDirectoryContents.mockImplementation( + mockClient.files.listDirectoryContentsIter.mockImplementation( async function* () {}, ); diff --git a/packages/appkit/src/plugins/files/tests/raw-endpoint.test.ts b/packages/appkit/src/plugins/files/tests/raw-endpoint.test.ts index 9a2e1587a..a288dbb27 100644 --- a/packages/appkit/src/plugins/files/tests/raw-endpoint.test.ts +++ b/packages/appkit/src/plugins/files/tests/raw-endpoint.test.ts @@ -28,12 +28,11 @@ describe("FilesPlugin raw endpoint security headers", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); @@ -48,7 +47,7 @@ describe("FilesPlugin raw endpoint security headers", () => { const handler = getRouteHandler(plugin, "get", "/raw"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse("data"), ); @@ -70,7 +69,7 @@ describe("FilesPlugin raw endpoint security headers", () => { const handler = getRouteHandler(plugin, "get", "/raw"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse("PNG data"), ); @@ -98,7 +97,7 @@ describe("FilesPlugin raw endpoint security headers", () => { const handler = getRouteHandler(plugin, "get", "/raw"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse(""), ); @@ -125,7 +124,7 @@ describe("FilesPlugin raw endpoint security headers", () => { const handler = getRouteHandler(plugin, "get", "/raw"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse(""), ); @@ -147,7 +146,7 @@ describe("FilesPlugin raw endpoint security headers", () => { const handler = getRouteHandler(plugin, "get", "/raw"); const res = mockRes(); - getMock(client, "files.download").mockResolvedValue( + getMock(client, "files.downloadFile").mockResolvedValue( makeStreamResponse("content"), ); diff --git a/packages/appkit/src/plugins/files/tests/shutdown.test.ts b/packages/appkit/src/plugins/files/tests/shutdown.test.ts index 31edbb8d0..d987336cf 100644 --- a/packages/appkit/src/plugins/files/tests/shutdown.test.ts +++ b/packages/appkit/src/plugins/files/tests/shutdown.test.ts @@ -16,12 +16,11 @@ describe("FilesPlugin shutdown and trackWrite", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); diff --git a/packages/appkit/src/plugins/files/tests/upload.test.ts b/packages/appkit/src/plugins/files/tests/upload.test.ts index 9aa0c6357..c483a59ca 100644 --- a/packages/appkit/src/plugins/files/tests/upload.test.ts +++ b/packages/appkit/src/plugins/files/tests/upload.test.ts @@ -24,12 +24,11 @@ describe("FilesPlugin upload", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); diff --git a/packages/appkit/src/plugins/files/tests/utils.ts b/packages/appkit/src/plugins/files/tests/utils.ts index beb947a87..9bd7b35d6 100644 --- a/packages/appkit/src/plugins/files/tests/utils.ts +++ b/packages/appkit/src/plugins/files/tests/utils.ts @@ -1 +1,4 @@ -export { streamFromString } from "../../../connectors/files/tests/utils"; +export { + headResponse, + streamFromString, +} from "../../../connectors/files/tests/utils"; diff --git a/packages/appkit/src/plugins/files/tests/volume-config.test.ts b/packages/appkit/src/plugins/files/tests/volume-config.test.ts index 8ddba4b88..c1250a6d9 100644 --- a/packages/appkit/src/plugins/files/tests/volume-config.test.ts +++ b/packages/appkit/src/plugins/files/tests/volume-config.test.ts @@ -17,12 +17,11 @@ describe("FilesPlugin volume config surface", () => { client = createMockWorkspaceClient({ strict: true, responses: { - "files.listDirectoryContents": undefined, - "files.download": undefined, - "files.getMetadata": undefined, + "files.listDirectoryContentsIter": undefined, + "files.downloadFile": undefined, "files.upload": undefined, "files.createDirectory": undefined, - "files.delete": undefined, + "files.deleteFile": undefined, }, }); serviceContextMock = await setupTestEnv(client); diff --git a/packages/appkit/src/plugins/files/types.ts b/packages/appkit/src/plugins/files/types.ts index ee58c8c04..6fb235c99 100644 --- a/packages/appkit/src/plugins/files/types.ts +++ b/packages/appkit/src/plugins/files/types.ts @@ -1,6 +1,5 @@ import type { BasePluginConfig, IAppRequest } from "shared"; -import type { files } from "../../workspace-client"; import type { FilePolicy } from "./policy"; /** @@ -159,11 +158,28 @@ export interface IFilesConfig extends BasePluginConfig { auth?: "service-principal" | "on-behalf-of-user"; } -/** A single entry returned when listing a directory. Re-exported from `@databricks/sdk-experimental`. */ -export type DirectoryEntry = files.DirectoryEntry; +/** A single entry returned when listing a directory (Files API wire shape). */ +export interface DirectoryEntry { + /** The length of the file in bytes. Omitted for directories. */ + file_size?: number; + /** True if the path is a directory. */ + is_directory?: boolean; + /** Last modification time in milliseconds since unix epoch. */ + last_modified?: number; + /** The name of the file or directory (last path component). */ + name?: string; + /** The absolute path of the file or directory. */ + path?: string; +} -/** Response object for file downloads containing a readable stream. Re-exported from `@databricks/sdk-experimental`. */ -export type DownloadResponse = files.DownloadResponse; +/** Response object for file downloads containing a readable stream. */ +export interface DownloadResponse { + "content-length"?: number; + "content-type"?: string; + contents?: ReadableStream; + /** Last modified time in HTTP-date (RFC 7231) format. */ + "last-modified"?: string; +} /** * Metadata for a file stored in a Unity Catalog volume. diff --git a/packages/shared/package.json b/packages/shared/package.json index 01938030b..451fbcbc0 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -51,6 +51,7 @@ "@databricks/sdk-auth": "0.51.0", "@databricks/sdk-core": "0.51.0", "@databricks/sdk-experimental": "0.17.0", + "@databricks/sdk-files": "0.52.0", "@databricks/sdk-genie": "0.54.0", "@databricks/sdk-jobs": "0.57.0", "@databricks/sdk-options": "0.51.0", diff --git a/packages/shared/src/workspace-client/client.ts b/packages/shared/src/workspace-client/client.ts index 92ca56b11..ff0074ca5 100644 --- a/packages/shared/src/workspace-client/client.ts +++ b/packages/shared/src/workspace-client/client.ts @@ -22,6 +22,8 @@ import { type GenieClient, buildJobsClient, type JobsClient, + buildFilesClient, + type FilesClient, type StatementExecutionClient, type WarehousesClient, type WorkspaceAuth, @@ -38,13 +40,18 @@ export class AppKitWorkspaceClient implements WorkspaceClient { #currentUser?: ScimClient; #genie?: GenieClient; #jobs?: JobsClient; + #files?: FilesClient; constructor(opts: WorkspaceClientOptions) { this.#opts = opts; } - get files() { - return this.#getLegacy().files; + // Migrated to the modular SDK β€” built lazily, independent of the legacy client. + get files(): FilesClient { + if (!this.#files) { + this.#files = buildFilesClient(this.#opts); + } + return this.#files; } // 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 619c24d43..74f08cb15 100644 --- a/packages/shared/src/workspace-client/index.ts +++ b/packages/shared/src/workspace-client/index.ts @@ -20,6 +20,6 @@ export { Time, TimeUnits, } from "./legacy"; -export type { files, jobs, serving, sql, WorkspaceClient } from "./types"; +export type { jobs, 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 43ca41374..d00540052 100644 --- a/packages/shared/src/workspace-client/modular.ts +++ b/packages/shared/src/workspace-client/modular.ts @@ -41,6 +41,7 @@ import { newFetchHttpClient, } from "@databricks/sdk-core/http"; import { resolve } from "@databricks/sdk-core/profiles"; +import { FilesClient } from "@databricks/sdk-files/v2"; import { GenieClient } from "@databricks/sdk-genie/v1"; import { JobsClient } from "@databricks/sdk-jobs/v2"; import type { ClientOptions } from "@databricks/sdk-options/client"; @@ -339,7 +340,18 @@ export function buildJobsClient(opts: WorkspaceClientOptions): JobsClient { return new JobsClient(mapToClientOptions(opts)); } +/** + * Build a modular Files client from wrapper options. `getFileMetadata` is not + * used: its generated HEAD call parses the (empty) body and drops the response + * headers that carry the metadata, so the connector issues that HEAD through + * {@link WorkspaceAuth.request} instead. + */ +export function buildFilesClient(opts: WorkspaceClientOptions): FilesClient { + return new FilesClient(mapToClientOptions(opts)); +} + // ── Client type re-exports (for the facade accessor types) ─────────────── +export type { FilesClient } from "@databricks/sdk-files/v2"; export type { GenieClient } from "@databricks/sdk-genie/v1"; export type { JobsClient } from "@databricks/sdk-jobs/v2"; export type { ScimClient } from "@databricks/sdk-scim/v1"; diff --git a/packages/shared/src/workspace-client/types.ts b/packages/shared/src/workspace-client/types.ts index 352b1780a..b737ab401 100644 --- a/packages/shared/src/workspace-client/types.ts +++ b/packages/shared/src/workspace-client/types.ts @@ -8,7 +8,7 @@ * later means changing that accessor's type here + its getter in `client.ts` * and updating the one connector that consumes it. * - * Type-namespace re-exports (`files`, `jobs`, `serving`, `sql`) point at + * Type-namespace re-exports (`jobs`, `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. @@ -21,6 +21,7 @@ import type { ScimClient, GenieClient, JobsClient, + FilesClient, } from "./modular"; // Legacy SDK type namespaces for un-migrated services, re-exported so AppKit @@ -34,8 +35,8 @@ import type { // `jobs` stays as the wire-shape (snake_case, `number` IDs) type of the jobs // plugin's public API; appkit's jobs connector translates the modular client's // camelCase/`bigint` models back to it. -export type { files, jobs, serving, sql } from "@databricks/sdk-experimental"; -// Modular SDK client + model types (warehouses, statementExecution, genie, jobs). +export type { jobs, serving, sql } from "@databricks/sdk-experimental"; +// Modular SDK client + model types (warehouses, statementExecution, genie, jobs, files). export type * from "./modular"; /** @@ -47,8 +48,8 @@ export type * from "./modular"; * client); see the module docblock. */ export interface WorkspaceClient extends WorkspaceAuth { - /** UC Volumes / Files API. */ - readonly files: LegacyWorkspaceClient["files"]; + /** UC Volumes / Files API (modular SDK). */ + readonly files: FilesClient; /** SQL Warehouses (modular SDK). */ readonly warehouses: WarehousesClient; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1b0f795e..3c6b8a5cf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,6 +289,9 @@ importers: '@databricks/sdk-experimental': specifier: 0.17.0 version: 0.17.0 + '@databricks/sdk-files': + specifier: 0.52.0 + version: 0.52.0 '@databricks/sdk-genie': specifier: 0.54.0 version: 0.54.0(patch_hash=664f3e13b155eb1b5499486a4f6fc1ab9a6a502e2c6b200d06749bd25dd9ae54) @@ -628,6 +631,9 @@ importers: '@databricks/sdk-experimental': specifier: 0.17.0 version: 0.17.0 + '@databricks/sdk-files': + specifier: 0.52.0 + version: 0.52.0 '@databricks/sdk-genie': specifier: 0.54.0 version: 0.54.0(patch_hash=664f3e13b155eb1b5499486a4f6fc1ab9a6a502e2c6b200d06749bd25dd9ae54) @@ -2027,6 +2033,10 @@ packages: resolution: {integrity: sha512-dOJIt4F2nBk6HKObnv7Xbmy/qLYTy2835qhXSuW0Qw1QAXui9plmCet1KqG3yeQcMTyncWGbnhjGdQi8GEGQSA==} engines: {node: '>=22.0', npm: '>=10.0.0'} + '@databricks/sdk-files@0.52.0': + resolution: {integrity: sha512-H3/uAkkl7EI+60/q1FRqtKrG7UdJfWKLug4/87q+P7OXJMF3J8kgECIg1LfU2Ih2Qj42LNhqdjos7LJSC68gyw==} + engines: {node: '>=22.0.0'} + '@databricks/sdk-genie@0.54.0': resolution: {integrity: sha512-xT+dyuXocgwPJ5VA1Kr5/dSdKzzR9hRAkEOybeO2i+v891kuuFzVd/M9OM31PtO2/YZp95vdFH5Sb+jVGtm4nw==} engines: {node: '>=22.0.0'} @@ -14347,6 +14357,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@databricks/sdk-files@0.52.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-genie@0.54.0(patch_hash=664f3e13b155eb1b5499486a4f6fc1ab9a6a502e2c6b200d06749bd25dd9ae54)': dependencies: '@databricks/sdk-auth': 0.51.0