diff --git a/.speakeasy/gen.yaml b/.speakeasy/gen.yaml index 6152b05f..07628c77 100644 --- a/.speakeasy/gen.yaml +++ b/.speakeasy/gen.yaml @@ -27,7 +27,8 @@ generation: versioningStrategy: automatic mockServer: disabled: false - persistentEdits: {} + persistentEdits: + enabled: true tests: generateTests: true generateNewTests: true diff --git a/README.md b/README.md index 603b3ad8..97a3818b 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Each namespace has its own authentication requirements and access patterns. Whil ```typescript // Example of accessing Client namespace const glean = new Glean({ - instance: 'instance-name', + serverURL: "https://mycompany-be.glean.com", apiToken: 'client-token' }); @@ -24,7 +24,7 @@ await glean.client.search.query({ // Example of accessing Indexing namespace const glean = new Glean({ - instance: 'instance-name', + serverURL: "https://mycompany-be.glean.com", apiToken: 'indexing-token' }); @@ -1169,7 +1169,7 @@ import { Glean } from "@gleanwork/api-client"; const glean = new Glean({ apiToken: process.env["GLEAN_API_TOKEN"] ?? "", - instance: process.env["GLEAN_INSTANCE"] ?? "", + serverURL: "https://mycompany-be.glean.com", }); ``` @@ -1182,7 +1182,7 @@ import type { XGleanOptions } from "@gleanwork/api-client/hooks/x-glean-options. const opts = { apiToken: process.env["GLEAN_API_TOKEN"] ?? "", - instance: process.env["GLEAN_INSTANCE"] ?? "", + serverURL: "https://mycompany-be.glean.com", excludeDeprecatedAfter: "2026-10-15", includeExperimental: true, } satisfies SDKOptions & XGleanOptions; diff --git a/src/__tests__/indexing.test-d.ts b/src/__tests__/indexing.test-d.ts index eec60088..2d3f1bb4 100644 --- a/src/__tests__/indexing.test-d.ts +++ b/src/__tests__/indexing.test-d.ts @@ -6,7 +6,7 @@ test("type test for `Glean` constructor", () => { async function run() { const glean = new Glean({ apiToken: process.env["GLEAN_API_TOKEN"], - instance: process.env["GLEAN_INSTANCE"], + serverURL: process.env["GLEAN_SERVER_URL"], }); const response = await glean.indexing.documents.index({ @@ -34,7 +34,7 @@ test("type test for `Glean` constructor", () => { const correctClient = new Glean({ apiToken: "token", - instance: "example-instance", + serverURL: "https://mycompany-be.glean.com", }); expectTypeOf(correctClient).toEqualTypeOf(); }); diff --git a/src/__tests__/server-url-normalizer.test.ts b/src/__tests__/server-url-normalizer.test.ts new file mode 100644 index 00000000..4a086564 --- /dev/null +++ b/src/__tests__/server-url-normalizer.test.ts @@ -0,0 +1,113 @@ +import { describe, expect, it } from "vitest"; +import { + normalizeServerURL, + serverURLNormalizerHook, +} from "../hooks/server-url-normalizer.js"; +import { HTTPClient } from "../lib/http.js"; +import { SDKInitOptions } from "../hooks/types.js"; +import { serverURLFromOptions } from "../lib/config.js"; + +describe("normalizeServerURL", () => { + it("should prepend https:// when no scheme is provided", () => { + expect(normalizeServerURL("example.glean.com")).toBe( + "https://example.glean.com", + ); + }); + + it("should preserve https:// scheme", () => { + expect(normalizeServerURL("https://example.glean.com")).toBe( + "https://example.glean.com", + ); + }); + + it("should preserve http:// for localhost", () => { + expect(normalizeServerURL("http://localhost:8080")).toBe( + "http://localhost:8080", + ); + }); + + it("should preserve http:// for non-localhost URLs", () => { + expect(normalizeServerURL("http://example.glean.com")).toBe( + "http://example.glean.com", + ); + }); + + it("should strip trailing slashes", () => { + expect(normalizeServerURL("https://example.glean.com/")).toBe( + "https://example.glean.com", + ); + }); + + it("should strip multiple trailing slashes", () => { + expect(normalizeServerURL("https://example.glean.com///")).toBe( + "https://example.glean.com", + ); + }); + + it("should preserve URLs with paths", () => { + expect(normalizeServerURL("https://example.glean.com/api/v1")).toBe( + "https://example.glean.com/api/v1", + ); + }); + + it("should handle no scheme with trailing slash", () => { + expect(normalizeServerURL("example.glean.com/")).toBe( + "https://example.glean.com", + ); + }); +}); + +describe("serverURLNormalizerHook", () => { + function createOpts(baseURL: string | null): SDKInitOptions { + return { + baseURL: baseURL ? new URL(baseURL) : null, + client: new HTTPClient(), + }; + } + + it("should normalize the baseURL when present", () => { + const opts = { + baseURL: new URL("https://example.glean.com/"), + client: new HTTPClient(), + }; + + const result = serverURLNormalizerHook.sdkInit(opts); + + expect(result.baseURL?.toString()).toBe("https://example.glean.com/"); + }); + + it("should return opts unchanged when baseURL is null", () => { + const opts = createOpts(null); + + const result = serverURLNormalizerHook.sdkInit(opts); + + expect(result.baseURL).toBeNull(); + }); +}); + +describe("serverURLFromOptions (config.ts normalization)", () => { + it("should handle schemeless serverURL", () => { + const url = serverURLFromOptions({ serverURL: "mycompany-be.glean.com" }); + expect(url?.origin).toBe("https://mycompany-be.glean.com"); + }); + + it("should handle serverURL with https scheme", () => { + const url = serverURLFromOptions({ serverURL: "https://mycompany-be.glean.com" }); + expect(url?.origin).toBe("https://mycompany-be.glean.com"); + }); + + it("should handle serverURL with http scheme", () => { + const url = serverURLFromOptions({ serverURL: "http://localhost:8080" }); + expect(url?.origin).toBe("http://localhost:8080"); + }); + + it("should handle serverURL with trailing slashes", () => { + const url = serverURLFromOptions({ serverURL: "https://mycompany-be.glean.com///" }); + expect(url?.origin).toBe("https://mycompany-be.glean.com"); + }); + + it("should handle schemeless serverURL with trailing slash", () => { + const url = serverURLFromOptions({ serverURL: "mycompany-be.glean.com/" }); + expect(url?.origin).toBe("https://mycompany-be.glean.com"); + }); +}); diff --git a/src/hooks/registration.ts b/src/hooks/registration.ts index 2b00fb65..287c71c4 100644 --- a/src/hooks/registration.ts +++ b/src/hooks/registration.ts @@ -1,5 +1,6 @@ import { Hooks, AfterErrorHook } from "./types.js"; import { XGlean } from "./x-glean.js"; +import { serverURLNormalizerHook } from "./server-url-normalizer.js"; /* * This file is only ever generated once on the first generation and then is free to be modified. @@ -47,6 +48,7 @@ export function initHooks(hooks: Hooks) { // Add hooks by calling hooks.register{ClientInit/BeforeCreateRequest/BeforeRequest/AfterSuccess/AfterError}Hook // with an instance of a hook that implements that specific Hook interface // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance + hooks.registerSDKInitHook(serverURLNormalizerHook); hooks.registerAfterErrorHook(agentFileUploadErrorHook); // Register the X-Glean header hook for experimental features and deprecation testing diff --git a/src/hooks/server-url-normalizer.ts b/src/hooks/server-url-normalizer.ts new file mode 100644 index 00000000..b376dc7e --- /dev/null +++ b/src/hooks/server-url-normalizer.ts @@ -0,0 +1,20 @@ +import { SDKInitHook, SDKInitOptions } from "./types.js"; + +export function normalizeServerURL(url: string): string { + let normalized = url; + if (!/^https?:\/\//i.test(normalized)) { + normalized = `https://${normalized}`; + } + normalized = normalized.replace(/\/+$/, ""); + return normalized; +} + +export const serverURLNormalizerHook: SDKInitHook = { + sdkInit(opts: SDKInitOptions): SDKInitOptions { + if (opts.baseURL) { + const normalized = normalizeServerURL(opts.baseURL.toString()); + opts.baseURL = new URL(normalized); + } + return opts; + }, +}; diff --git a/src/lib/config.ts b/src/lib/config.ts index 715aecd5..9399728d 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -61,7 +61,11 @@ export function serverURLFromOptions(options: SDKOptions): URL | null { params = serverParams[serverIdx] || {}; } - const u = pathToFunc(serverURL)(params); + let u = pathToFunc(serverURL)(params); + if (u && !/^https?:\/\//i.test(u)) { + u = `https://${u}`; + } + u = u.replace(/\/+$/, ""); return new URL(u); }