Skip to content

Commit bc4bce1

Browse files
authored
fix: use URL type for manipulating urls (#1417)
* fix: merge global passed headers with default headers * refactor: use URL type for manipulating urls * use baeUrl directly * use href
1 parent 70d7199 commit bc4bce1

File tree

5 files changed

+184
-100
lines changed

5 files changed

+184
-100
lines changed

src/SupabaseClient.ts

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,10 @@ export default class SupabaseClient<
4343
auth: SupabaseAuthClient
4444
realtime: RealtimeClient
4545

46-
protected realtimeUrl: string
47-
protected authUrl: string
48-
protected storageUrl: string
49-
protected functionsUrl: string
46+
protected realtimeUrl: URL
47+
protected authUrl: URL
48+
protected storageUrl: URL
49+
protected functionsUrl: URL
5050
protected rest: PostgrestClient<Database, SchemaName, Schema>
5151
protected storageKey: string
5252
protected fetch?: Fetch
@@ -76,14 +76,16 @@ export default class SupabaseClient<
7676
if (!supabaseKey) throw new Error('supabaseKey is required.')
7777

7878
const _supabaseUrl = stripTrailingSlash(supabaseUrl)
79+
const baseUrl = new URL(_supabaseUrl)
7980

80-
this.realtimeUrl = `${_supabaseUrl}/realtime/v1`.replace(/^http/i, 'ws')
81-
this.authUrl = `${_supabaseUrl}/auth/v1`
82-
this.storageUrl = `${_supabaseUrl}/storage/v1`
83-
this.functionsUrl = `${_supabaseUrl}/functions/v1`
81+
this.realtimeUrl = new URL('/realtime/v1', baseUrl)
82+
this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws')
83+
this.authUrl = new URL('/auth/v1', baseUrl)
84+
this.storageUrl = new URL('/storage/v1', baseUrl)
85+
this.functionsUrl = new URL('/functions/v1', baseUrl)
8486

8587
// default storage key uses the supabase project ref as a namespace
86-
const defaultStorageKey = `sb-${new URL(this.authUrl).hostname.split('.')[0]}-auth-token`
88+
const defaultStorageKey = `sb-${baseUrl.hostname.split('.')[0]}-auth-token`
8789
const DEFAULTS = {
8890
db: DEFAULT_DB_OPTIONS,
8991
realtime: DEFAULT_REALTIME_OPTIONS,
@@ -137,7 +139,7 @@ export default class SupabaseClient<
137139
* Supabase Functions allows you to deploy and invoke edge functions.
138140
*/
139141
get functions(): FunctionsClient {
140-
return new FunctionsClient(this.functionsUrl, {
142+
return new FunctionsClient(this.functionsUrl.href, {
141143
headers: this.headers,
142144
customFetch: this.fetch,
143145
})
@@ -147,7 +149,7 @@ export default class SupabaseClient<
147149
* Supabase Storage allows you to manage user-generated content, such as photos or videos.
148150
*/
149151
get storage(): SupabaseStorageClient {
150-
return new SupabaseStorageClient(this.storageUrl, this.headers, this.fetch)
152+
return new SupabaseStorageClient(this.storageUrl.href, this.headers, this.fetch)
151153
}
152154

153155
// NOTE: signatures must be kept in sync with PostgrestClient.from
@@ -295,7 +297,7 @@ export default class SupabaseClient<
295297
apikey: `${this.supabaseKey}`,
296298
}
297299
return new SupabaseAuthClient({
298-
url: this.authUrl,
300+
url: this.authUrl.href,
299301
headers: { ...authHeaders, ...headers },
300302
storageKey: storageKey,
301303
autoRefreshToken,
@@ -313,7 +315,7 @@ export default class SupabaseClient<
313315
}
314316

315317
private _initRealtimeClient(options: RealtimeClientOptions) {
316-
return new RealtimeClient(this.realtimeUrl, {
318+
return new RealtimeClient(this.realtimeUrl.href, {
317319
...options,
318320
params: { ...{ apikey: this.supabaseKey }, ...options?.params },
319321
})

src/lib/helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ export function applySettingDefaults<
5353
global: {
5454
...DEFAULT_GLOBAL_OPTIONS,
5555
...globalOptions,
56+
headers: {
57+
...(DEFAULT_GLOBAL_OPTIONS?.headers ?? {}),
58+
...(globalOptions?.headers ?? {}),
59+
},
5660
},
5761
accessToken: async () => '',
5862
}

test/SupabaseClient.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { PostgrestClient } from '@supabase/postgrest-js'
2+
import { createClient, SupabaseClient } from '../src/index'
3+
import { Database } from './types'
4+
5+
const URL = 'http://localhost:3000'
6+
const KEY = 'some.fake.key'
7+
8+
describe('SupabaseClient', () => {
9+
test('it should create a client with third-party auth accessToken', async () => {
10+
const client = createClient(URL, KEY, {
11+
accessToken: async () => {
12+
return 'jwt'
13+
},
14+
})
15+
16+
expect(() => client.auth.getUser()).toThrow(
17+
'@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.getUser is not possible'
18+
)
19+
})
20+
21+
test('it should create the client connection', async () => {
22+
const supabase = createClient(URL, KEY)
23+
expect(supabase).toBeDefined()
24+
expect(supabase).toBeInstanceOf(SupabaseClient)
25+
})
26+
27+
test('it should throw an error if no valid params are provided', async () => {
28+
expect(() => createClient('', KEY)).toThrow('supabaseUrl is required.')
29+
expect(() => createClient(URL, '')).toThrow('supabaseKey is required.')
30+
})
31+
32+
describe('URL Construction', () => {
33+
test('should construct URLs correctly', () => {
34+
const client = createClient(URL, KEY)
35+
36+
// @ts-ignore
37+
expect(client.authUrl.toString()).toEqual('http://localhost:3000/auth/v1')
38+
// @ts-ignore
39+
expect(client.realtimeUrl.toString()).toEqual('ws://localhost:3000/realtime/v1')
40+
// @ts-ignore
41+
expect(client.storageUrl.toString()).toEqual('http://localhost:3000/storage/v1')
42+
// @ts-ignore
43+
expect(client.functionsUrl.toString()).toEqual('http://localhost:3000/functions/v1')
44+
// @ts-ignore
45+
expect(client.rest.url).toEqual('http://localhost:3000/rest/v1')
46+
})
47+
48+
test('should handle HTTPS URLs correctly', () => {
49+
const client = createClient('https://localhost:3000', KEY)
50+
// @ts-ignore
51+
expect(client.realtimeUrl.toString()).toEqual('wss://localhost:3000/realtime/v1')
52+
})
53+
})
54+
55+
describe('Custom Headers', () => {
56+
test('should have custom header set', () => {
57+
const customHeader = { 'X-Test-Header': 'value' }
58+
const request = createClient(URL, KEY, { global: { headers: customHeader } }).rpc('')
59+
// @ts-ignore
60+
const getHeaders = request.headers
61+
expect(getHeaders).toHaveProperty('X-Test-Header', 'value')
62+
})
63+
64+
test('should merge custom headers with default headers', () => {
65+
const customHeader = { 'X-Test-Header': 'value' }
66+
const client = createClient(URL, KEY, { global: { headers: customHeader } })
67+
// @ts-ignore
68+
expect(client.headers).toHaveProperty('X-Test-Header', 'value')
69+
// @ts-ignore
70+
expect(client.headers).toHaveProperty('X-Client-Info')
71+
})
72+
})
73+
74+
describe('Storage Key', () => {
75+
test('should use default storage key based on project ref', () => {
76+
const client = createClient('https://project-ref.supabase.co', KEY)
77+
// @ts-ignore
78+
expect(client.storageKey).toBe('sb-project-ref-auth-token')
79+
})
80+
81+
test('should use custom storage key when provided', () => {
82+
const customStorageKey = 'custom-storage-key'
83+
const client = createClient(URL, KEY, {
84+
auth: { storageKey: customStorageKey },
85+
})
86+
// @ts-ignore
87+
expect(client.storageKey).toBe(customStorageKey)
88+
})
89+
})
90+
91+
describe('Client Methods', () => {
92+
test('should initialize functions client', () => {
93+
const client = createClient(URL, KEY)
94+
const functions = client.functions
95+
expect(functions).toBeDefined()
96+
// @ts-ignore
97+
expect(functions.url).toBe('http://localhost:3000/functions/v1')
98+
})
99+
100+
test('should initialize storage client', () => {
101+
const client = createClient(URL, KEY)
102+
const storage = client.storage
103+
expect(storage).toBeDefined()
104+
// @ts-ignore
105+
expect(storage.url).toBe('http://localhost:3000/storage/v1')
106+
})
107+
108+
test('should initialize realtime client', () => {
109+
const client = createClient(URL, KEY)
110+
expect(client.realtime).toBeDefined()
111+
// @ts-ignore
112+
expect(client.realtime.endPoint).toBe('ws://localhost:3000/realtime/v1/websocket')
113+
})
114+
})
115+
116+
describe('Realtime Channel Management', () => {
117+
test('should create and manage channels', () => {
118+
const client = createClient(URL, KEY)
119+
const channel = client.channel('test-channel')
120+
expect(channel).toBeDefined()
121+
expect(client.getChannels()).toHaveLength(1)
122+
})
123+
124+
test('should remove channel', async () => {
125+
const client = createClient(URL, KEY)
126+
const channel = client.channel('test-channel')
127+
const result = await client.removeChannel(channel)
128+
expect(result).toBe('ok')
129+
expect(client.getChannels()).toHaveLength(0)
130+
})
131+
132+
test('should remove all channels', async () => {
133+
const client = createClient(URL, KEY)
134+
client.channel('channel1')
135+
client.channel('channel2')
136+
const results = await client.removeAllChannels()
137+
expect(results).toEqual(['ok', 'ok'])
138+
expect(client.getChannels()).toHaveLength(0)
139+
})
140+
})
141+
142+
describe('Schema Management', () => {
143+
test('should switch schema', () => {
144+
const client = createClient<Database>(URL, KEY)
145+
const schemaClient = client.schema('personal')
146+
expect(schemaClient).toBeDefined()
147+
expect(schemaClient).toBeInstanceOf(PostgrestClient)
148+
})
149+
})
150+
151+
describe('RPC Calls', () => {
152+
test('should make RPC call with arguments', () => {
153+
const client = createClient<Database>(URL, KEY)
154+
const rpcCall = client.rpc('get_status', { name_param: 'test' })
155+
expect(rpcCall).toBeDefined()
156+
})
157+
158+
test('should make RPC call with options', () => {
159+
const client = createClient<Database>(URL, KEY)
160+
const rpcCall = client.rpc('get_status', { name_param: 'test' }, { head: true })
161+
expect(rpcCall).toBeDefined()
162+
})
163+
})
164+
})

test/client.test.ts

Lines changed: 0 additions & 86 deletions
This file was deleted.

test/helpers.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ test('override setting defaults', async () => {
3636
expect(settings.auth.autoRefreshToken).toBe(autoRefreshOption)
3737
// Existing default properties should not be overwritten
3838
expect(settings.auth.persistSession).not.toBeNull()
39-
expect(settings.global.headers).toBe(DEFAULT_HEADERS)
39+
expect(settings.global.headers).toStrictEqual(DEFAULT_HEADERS)
4040
// Existing property values should remain constant
4141
expect(settings.db.schema).toBe(defaults.db.schema)
4242
})

0 commit comments

Comments
 (0)