Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .speakeasy/gen.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ generation:
versioningStrategy: automatic
mockServer:
disabled: false
persistentEdits: {}
persistentEdits:
enabled: true
tests:
generateTests: true
generateNewTests: true
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});

Expand All @@ -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'
});

Expand Down Expand Up @@ -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",
});
```

Expand All @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/__tests__/indexing.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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<Glean>();
});
113 changes: 113 additions & 0 deletions src/__tests__/server-url-normalizer.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
2 changes: 2 additions & 0 deletions src/hooks/registration.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/server-url-normalizer.ts
Original file line number Diff line number Diff line change
@@ -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;
},
};
6 changes: 5 additions & 1 deletion src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down