Skip to content

Commit 52c7797

Browse files
CopilotMathiasVDA
andauthored
fix: relative endpoint URL resolution when saving to SPARQL workspace (#118)
* Initial plan * Add resolveEndpointUrl utility and update Tab.ts to resolve relative endpoints Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> * Address code review feedback: use startsWith() and remove edge case test Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> * Improve variable naming: rename basePath to currentDirectory for clarity Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> * fix: quick describe didn't always work for prefixed URIs (#120) * Initial plan * Fix quick describe for tokenized prefixed URIs Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> * Address code review feedback - use template literals and improved parsing Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MathiasVDA <15101339+MathiasVDA@users.noreply.github.com>
1 parent e62a646 commit 52c7797

3 files changed

Lines changed: 128 additions & 1 deletion

File tree

packages/yasgui/src/Tab.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { saveManagedQuery } from "./queryManagement/saveManagedQuery";
1717
import { getWorkspaceBackend } from "./queryManagement/backends/getWorkspaceBackend";
1818
import { asWorkspaceBackendError } from "./queryManagement/backends/errors";
1919
import { normalizeQueryFilename } from "./queryManagement/normalizeQueryFilename";
20+
import { resolveEndpointUrl } from "./urlUtils";
2021

2122
export interface PersistedJsonYasr extends YasrPersistentConfig {
2223
responseSummary: Parser.ResponseSummary;
@@ -304,7 +305,7 @@ export class Tab extends EventEmitter {
304305
name: result.name,
305306
filename: result.filename,
306307
queryText: this.getQueryTextForSave(),
307-
associatedEndpoint: workspace.type === "sparql" ? this.getEndpoint() : undefined,
308+
associatedEndpoint: workspace.type === "sparql" ? resolveEndpointUrl(this.getEndpoint()) : undefined,
308309
message: result.message,
309310
expectedVersionTag,
310311
});

packages/yasgui/src/urlUtils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/**
2+
* Converts a relative or absolute URL to a fully qualified URL with protocol and host.
3+
* Uses the current page's protocol and host for relative URLs.
4+
*
5+
* @param url - The URL to resolve (can be relative like "/sparql", or absolute like "http://example.com/sparql")
6+
* @returns The fully qualified URL with protocol and host
7+
*
8+
* @example
9+
* // On page https://example.com/yasgui/
10+
* resolveEndpointUrl("/sparql") // returns "https://example.com/sparql"
11+
* resolveEndpointUrl("sparql") // returns "https://example.com/yasgui/sparql"
12+
* resolveEndpointUrl("http://other.com/sparql") // returns "http://other.com/sparql"
13+
*/
14+
export function resolveEndpointUrl(url: string): string {
15+
if (!url) return url;
16+
17+
// If URL already has a protocol (http: or https:), return as-is
18+
if (url.startsWith("http://") || url.startsWith("https://")) {
19+
return url;
20+
}
21+
22+
// Build the base URL using current page's protocol and host
23+
let fullUrl = `${window.location.protocol}//${window.location.host}`;
24+
25+
if (url.startsWith("/")) {
26+
// Absolute path (starts with /)
27+
fullUrl += url;
28+
} else {
29+
// Relative path - join with current page's directory
30+
let currentDirectory = window.location.pathname;
31+
// If pathname does not end with "/", treat it as a file and use its directory
32+
if (!currentDirectory.endsWith("/")) {
33+
const lastSlashIndex = currentDirectory.lastIndexOf("/");
34+
currentDirectory = lastSlashIndex >= 0 ? currentDirectory.substring(0, lastSlashIndex + 1) : "/";
35+
}
36+
fullUrl += currentDirectory + url;
37+
}
38+
39+
return fullUrl;
40+
}

test/unit/url-utils-test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as chai from "chai";
2+
import { describe, it, beforeEach, afterEach } from "mocha";
3+
4+
import { resolveEndpointUrl } from "../../packages/yasgui/src/urlUtils.js";
5+
6+
const expect = chai.expect;
7+
8+
describe("URL utilities - resolveEndpointUrl", () => {
9+
let originalWindow: any;
10+
11+
beforeEach(() => {
12+
// Save the original window object if it exists
13+
originalWindow = (global as any).window;
14+
});
15+
16+
afterEach(() => {
17+
// Restore the original window object
18+
if (originalWindow) {
19+
(global as any).window = originalWindow;
20+
} else {
21+
delete (global as any).window;
22+
}
23+
});
24+
25+
function mockWindow(url: string) {
26+
const parsedUrl = new URL(url);
27+
(global as any).window = {
28+
location: {
29+
protocol: parsedUrl.protocol,
30+
host: parsedUrl.host,
31+
hostname: parsedUrl.hostname,
32+
port: parsedUrl.port,
33+
pathname: parsedUrl.pathname,
34+
href: url,
35+
},
36+
};
37+
}
38+
39+
it("returns absolute URLs unchanged (http)", () => {
40+
mockWindow("https://example.com/yasgui/");
41+
const result = resolveEndpointUrl("http://example.com/sparql");
42+
expect(result).to.equal("http://example.com/sparql");
43+
});
44+
45+
it("returns absolute URLs unchanged (https)", () => {
46+
mockWindow("https://example.com/yasgui/");
47+
const result = resolveEndpointUrl("https://example.com/sparql");
48+
expect(result).to.equal("https://example.com/sparql");
49+
});
50+
51+
it("converts absolute path to full URL with current protocol and host", () => {
52+
mockWindow("https://example.com/yasgui/index.html");
53+
const result = resolveEndpointUrl("/sparql");
54+
expect(result).to.equal("https://example.com/sparql");
55+
});
56+
57+
it("converts relative path to full URL with current directory", () => {
58+
mockWindow("https://example.com/yasgui/");
59+
const result = resolveEndpointUrl("sparql");
60+
expect(result).to.equal("https://example.com/yasgui/sparql");
61+
});
62+
63+
it("uses https protocol when page is https", () => {
64+
mockWindow("https://secure.example.com/app/");
65+
const result = resolveEndpointUrl("/sparql");
66+
expect(result).to.equal("https://secure.example.com/sparql");
67+
});
68+
69+
it("uses http protocol when page is http", () => {
70+
mockWindow("http://example.com/app/");
71+
const result = resolveEndpointUrl("/sparql");
72+
expect(result).to.equal("http://example.com/sparql");
73+
});
74+
75+
it("handles empty string", () => {
76+
mockWindow("https://example.com/app/");
77+
const result = resolveEndpointUrl("");
78+
expect(result).to.equal("");
79+
});
80+
81+
it("includes port number if present", () => {
82+
mockWindow("https://example.com:8080/app/");
83+
const result = resolveEndpointUrl("/sparql");
84+
expect(result).to.equal("https://example.com:8080/sparql");
85+
});
86+
});

0 commit comments

Comments
 (0)