Skip to content

Commit 7186869

Browse files
authored
Merge pull request #18432 from Budibase/fix/datasource-base-url
Fix for base URL config in templated requests
2 parents 8a352ef + d2f3a75 commit 7186869

File tree

10 files changed

+597
-299
lines changed

10 files changed

+597
-299
lines changed

packages/builder/src/components/integration/APIEndpointViewer.svelte

Lines changed: 129 additions & 144 deletions
Large diffs are not rendered by default.

packages/builder/src/components/integration/CustomEndpointInput.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import { clickOutside, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
44
import APIEndpointVerbBadge from "./APIEndpointVerbBadge.svelte"
55
import { customQueryIconColor } from "@/helpers/data/utils"
6-
import { applyBaseUrl } from "./query"
6+
import { applyBaseUrl } from "@budibase/shared-core"
77
88
export let verb: string = "read"
99
export let url: string = ""
Lines changed: 71 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,22 @@
11
import { it, expect, describe } from "vitest"
2-
import { applyBaseUrl, isValidEndpointUrl } from "./query"
3-
2+
import { applyBaseUrl } from "@budibase/shared-core"
3+
import {
4+
isValidEndpointUrl,
5+
constructFullPath,
6+
convertPathVariables,
7+
validateQuery,
8+
} from "./query"
9+
10+
// Just a smoke test in the builder as the base functionality
11+
// is already covered
412
describe("applyBaseUrl", () => {
5-
describe("plain URLs", () => {
6-
it("replaces the origin, keeping the path", () => {
7-
expect(
8-
applyBaseUrl(
9-
"https://old.example.com/api/users",
10-
"https://new.example.com"
11-
)
12-
).toBe("https://new.example.com/api/users")
13-
})
14-
15-
it("replaces the origin, keeping the query string", () => {
16-
expect(
17-
applyBaseUrl(
18-
"https://old.example.com/api/users?page=1",
19-
"https://new.example.com"
20-
)
21-
).toBe("https://new.example.com/api/users?page=1")
22-
})
23-
24-
it("replaces the origin, keeping path + query string + hash", () => {
25-
expect(
26-
applyBaseUrl(
27-
"https://old.example.com/api/users?page=1#top",
28-
"https://new.example.com"
29-
)
30-
).toBe("https://new.example.com/api/users?page=1#top")
31-
})
32-
33-
it("drops the path when current URL is just an origin with no path", () => {
34-
expect(
35-
applyBaseUrl("https://old.example.com", "https://new.example.com")
36-
).toBe("https://new.example.com")
37-
})
38-
39-
it("falls back to newBase when currentUrl is not a valid URL", () => {
40-
expect(applyBaseUrl("not a url", "https://new.example.com")).toBe(
41-
"https://new.example.com"
42-
)
43-
})
44-
45-
it("falls back to newBase when currentUrl is empty", () => {
46-
expect(applyBaseUrl("", "https://new.example.com")).toBe(
13+
it("replaces the origin keeping the path", () => {
14+
expect(
15+
applyBaseUrl(
16+
"https://old.example.com/api/users",
4717
"https://new.example.com"
4818
)
49-
})
50-
51-
it("preserves a relative path when currentUrl has no origin", () => {
52-
expect(applyBaseUrl("/api/v1/users", "https://new.example.com")).toBe(
53-
"https://new.example.com/api/v1/users"
54-
)
55-
})
56-
57-
it("strips a trailing slash from newBase to avoid double slashes", () => {
58-
expect(
59-
applyBaseUrl(
60-
"https://old.example.com/api/users",
61-
"https://new.example.com/"
62-
)
63-
).toBe("https://new.example.com/api/users")
64-
})
65-
66-
it("strips trailing slash from newBase when preserving a relative path", () => {
67-
expect(applyBaseUrl("/api/v1/users", "https://new.example.com/")).toBe(
68-
"https://new.example.com/api/v1/users"
69-
)
70-
})
71-
})
72-
73-
describe("HBS template URLs", () => {
74-
it("preserves HBS blocks in the path", () => {
75-
expect(
76-
applyBaseUrl(
77-
"https://old.example.com/api/{{version}}/users",
78-
"https://new.example.com"
79-
)
80-
).toBe("https://new.example.com/api/{{version}}/users")
81-
})
82-
83-
it("preserves HBS blocks in the query string", () => {
84-
expect(
85-
applyBaseUrl(
86-
"https://old.example.com/api/users?token={{auth.token}}",
87-
"https://new.example.com"
88-
)
89-
).toBe("https://new.example.com/api/users?token={{auth.token}}")
90-
})
91-
92-
it("preserves an HBS block in the port position", () => {
93-
expect(
94-
applyBaseUrl(
95-
"http://{{host}}:{{port}}/api/users",
96-
"https://new.example.com"
97-
)
98-
).toBe("https://new.example.com/api/users")
99-
})
100-
101-
it("preserves HBS blocks in both port and path", () => {
102-
expect(
103-
applyBaseUrl(
104-
"http://{{host}}:{{port}}/api/{{version}}/users",
105-
"https://new.example.com"
106-
)
107-
).toBe("https://new.example.com/api/{{version}}/users")
108-
})
109-
110-
it("preserves multiple HBS blocks in the path", () => {
111-
expect(
112-
applyBaseUrl(
113-
"https://old.example.com/{{org}}/{{repo}}/issues",
114-
"https://new.example.com"
115-
)
116-
).toBe("https://new.example.com/{{org}}/{{repo}}/issues")
117-
})
19+
).toBe("https://new.example.com/api/users")
11820
})
11921
})
12022

@@ -189,3 +91,59 @@ describe("isValidEndpointUrl - HBS bindings", () => {
18991
expect(isValidEndpointUrl("{{derp")).toBe(false)
19092
})
19193
})
94+
95+
describe("convertPathVariables", () => {
96+
it("converts OpenAPI {var} to HBS {{var}}", () => {
97+
expect(convertPathVariables("/api/{version}/users/{id}")).toBe(
98+
"/api/{{version}}/users/{{id}}"
99+
)
100+
})
101+
102+
it("returns value unchanged when no path variables", () => {
103+
expect(convertPathVariables("/api/v1/users")).toBe("/api/v1/users")
104+
})
105+
})
106+
107+
describe("constructFullPath", () => {
108+
it("joins base URL and endpoint path", () => {
109+
expect(constructFullPath("https://example.com", "/api/v1/users")).toBe(
110+
"https://example.com/api/v1/users"
111+
)
112+
})
113+
114+
it("strips trailing slash from base", () => {
115+
expect(constructFullPath("https://example.com/", "/api/v1/users")).toBe(
116+
"https://example.com/api/v1/users"
117+
)
118+
})
119+
120+
it("returns base when path is empty", () => {
121+
expect(constructFullPath("https://example.com", "")).toBe(
122+
"https://example.com"
123+
)
124+
})
125+
126+
it("returns just the path when base is undefined", () => {
127+
expect(constructFullPath(undefined, "api/v1/users")).toBe("api/v1/users")
128+
})
129+
})
130+
131+
describe("validateQuery", () => {
132+
it("throws when url contains {{user}} binding", () => {
133+
expect(() =>
134+
validateQuery("https://example.com/{{user}}", undefined, {}, {})
135+
).toThrow("'user' is a protected binding")
136+
})
137+
138+
it("throws when request body contains {{user.id}}", () => {
139+
expect(() =>
140+
validateQuery("https://example.com/api", "{{user.id}}", {}, {})
141+
).toThrow("'user' is a protected binding")
142+
})
143+
144+
it("does not throw for safe bindings", () => {
145+
expect(() =>
146+
validateQuery("https://example.com/{{version}}/users", undefined, {}, {})
147+
).not.toThrow()
148+
})
149+
})

packages/builder/src/components/integration/query.ts

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { findHBSBlocks } from "@budibase/string-templates"
2-
import { v4 as uuid } from "uuid"
32
import restUtils from "@/helpers/data/utils"
43
import {
54
runtimeToReadableMap,
@@ -486,42 +485,3 @@ export function isValidEndpointUrl(url: string | undefined): boolean {
486485
return false
487486
}
488487
}
489-
490-
/**
491-
* Replaces the origin of `currentUrl` with `newBase`, preserving the path,
492-
* query string, hash, and any HBS template blocks (including port-position ones).
493-
*/
494-
export function applyBaseUrl(currentUrl: string, newBase: string): string {
495-
const nonce = uuid().replace(/-/g, "")
496-
const blocks = findHBSBlocks(currentUrl)
497-
const portBlocks: string[] = []
498-
499-
// Substitute HBS blocks in port position with ':0' so new URL() can parse it
500-
let parseable = currentUrl
501-
parseable = parseable.replace(/:(\{\{[^}]+\}\})/g, (_, block) => {
502-
portBlocks.push(block)
503-
return ":0"
504-
})
505-
506-
const placeholder = (i: number) => `hbs${nonce}${i}`
507-
const pathBlocks = blocks.filter(b => !portBlocks.includes(b))
508-
parseable = pathBlocks.reduce(
509-
(s, block, i) => s.replace(block, placeholder(i)),
510-
parseable
511-
)
512-
513-
const restore = (s: string) =>
514-
pathBlocks.reduce((r, block, i) => r.replace(placeholder(i), block), s)
515-
516-
const base = newBase.replace(/\/$/, "")
517-
try {
518-
const parsed = new URL(parseable)
519-
const path = parsed.pathname === "/" ? "" : parsed.pathname
520-
return base + restore(path + parsed.search + parsed.hash)
521-
} catch {
522-
if (parseable.startsWith("/")) {
523-
return base + restore(parseable)
524-
}
525-
return base
526-
}
527-
}

0 commit comments

Comments
 (0)