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
28 changes: 15 additions & 13 deletions src/SupabaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,10 @@ export default class SupabaseClient<
auth: SupabaseAuthClient
realtime: RealtimeClient

protected realtimeUrl: string
protected authUrl: string
protected storageUrl: string
protected functionsUrl: string
protected realtimeUrl: URL
protected authUrl: URL
protected storageUrl: URL
protected functionsUrl: URL
protected rest: PostgrestClient<Database, SchemaName, Schema>
protected storageKey: string
protected fetch?: Fetch
Expand Down Expand Up @@ -76,14 +76,16 @@ export default class SupabaseClient<
if (!supabaseKey) throw new Error('supabaseKey is required.')

const _supabaseUrl = stripTrailingSlash(supabaseUrl)
const baseUrl = new URL(_supabaseUrl)

this.realtimeUrl = `${_supabaseUrl}/realtime/v1`.replace(/^http/i, 'ws')
this.authUrl = `${_supabaseUrl}/auth/v1`
this.storageUrl = `${_supabaseUrl}/storage/v1`
this.functionsUrl = `${_supabaseUrl}/functions/v1`
this.realtimeUrl = new URL('/realtime/v1', baseUrl)
this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was gonna say just do = 'ws:' but I see what you did. ✅

this.authUrl = new URL('/auth/v1', baseUrl)
this.storageUrl = new URL('/storage/v1', baseUrl)
this.functionsUrl = new URL('/functions/v1', baseUrl)

// default storage key uses the supabase project ref as a namespace
const defaultStorageKey = `sb-${new URL(this.authUrl).hostname.split('.')[0]}-auth-token`
const defaultStorageKey = `sb-${baseUrl.hostname.split('.')[0]}-auth-token`
const DEFAULTS = {
db: DEFAULT_DB_OPTIONS,
realtime: DEFAULT_REALTIME_OPTIONS,
Expand Down Expand Up @@ -137,7 +139,7 @@ export default class SupabaseClient<
* Supabase Functions allows you to deploy and invoke edge functions.
*/
get functions(): FunctionsClient {
return new FunctionsClient(this.functionsUrl, {
return new FunctionsClient(this.functionsUrl.href, {
headers: this.headers,
customFetch: this.fetch,
})
Expand All @@ -147,7 +149,7 @@ export default class SupabaseClient<
* Supabase Storage allows you to manage user-generated content, such as photos or videos.
*/
get storage(): SupabaseStorageClient {
return new SupabaseStorageClient(this.storageUrl, this.headers, this.fetch)
return new SupabaseStorageClient(this.storageUrl.href, this.headers, this.fetch)
}

// NOTE: signatures must be kept in sync with PostgrestClient.from
Expand Down Expand Up @@ -295,7 +297,7 @@ export default class SupabaseClient<
apikey: `${this.supabaseKey}`,
}
return new SupabaseAuthClient({
url: this.authUrl,
url: this.authUrl.href,
headers: { ...authHeaders, ...headers },
storageKey: storageKey,
autoRefreshToken,
Expand All @@ -313,7 +315,7 @@ export default class SupabaseClient<
}

private _initRealtimeClient(options: RealtimeClientOptions) {
return new RealtimeClient(this.realtimeUrl, {
return new RealtimeClient(this.realtimeUrl.href, {
...options,
params: { ...{ apikey: this.supabaseKey }, ...options?.params },
})
Expand Down
4 changes: 4 additions & 0 deletions src/lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export function applySettingDefaults<
global: {
...DEFAULT_GLOBAL_OPTIONS,
...globalOptions,
headers: {
...(DEFAULT_GLOBAL_OPTIONS?.headers ?? {}),
...(globalOptions?.headers ?? {}),
},
},
accessToken: async () => '',
}
Expand Down
164 changes: 164 additions & 0 deletions test/SupabaseClient.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { PostgrestClient } from '@supabase/postgrest-js'
import { createClient, SupabaseClient } from '../src/index'
import { Database } from './types'

const URL = 'http://localhost:3000'
const KEY = 'some.fake.key'

describe('SupabaseClient', () => {
test('it should create a client with third-party auth accessToken', async () => {
const client = createClient(URL, KEY, {
accessToken: async () => {
return 'jwt'
},
})

expect(() => client.auth.getUser()).toThrow(
'@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.getUser is not possible'
)
})

test('it should create the client connection', async () => {
const supabase = createClient(URL, KEY)
expect(supabase).toBeDefined()
expect(supabase).toBeInstanceOf(SupabaseClient)
})

test('it should throw an error if no valid params are provided', async () => {
expect(() => createClient('', KEY)).toThrow('supabaseUrl is required.')
expect(() => createClient(URL, '')).toThrow('supabaseKey is required.')
})

describe('URL Construction', () => {
test('should construct URLs correctly', () => {
const client = createClient(URL, KEY)

// @ts-ignore
expect(client.authUrl.toString()).toEqual('http://localhost:3000/auth/v1')
// @ts-ignore
expect(client.realtimeUrl.toString()).toEqual('ws://localhost:3000/realtime/v1')
// @ts-ignore
expect(client.storageUrl.toString()).toEqual('http://localhost:3000/storage/v1')
// @ts-ignore
expect(client.functionsUrl.toString()).toEqual('http://localhost:3000/functions/v1')
// @ts-ignore
expect(client.rest.url).toEqual('http://localhost:3000/rest/v1')
})

test('should handle HTTPS URLs correctly', () => {
const client = createClient('https://localhost:3000', KEY)
// @ts-ignore
expect(client.realtimeUrl.toString()).toEqual('wss://localhost:3000/realtime/v1')
})
})

describe('Custom Headers', () => {
test('should have custom header set', () => {
const customHeader = { 'X-Test-Header': 'value' }
const request = createClient(URL, KEY, { global: { headers: customHeader } }).rpc('')
// @ts-ignore
const getHeaders = request.headers
expect(getHeaders).toHaveProperty('X-Test-Header', 'value')
})

test('should merge custom headers with default headers', () => {
const customHeader = { 'X-Test-Header': 'value' }
const client = createClient(URL, KEY, { global: { headers: customHeader } })
// @ts-ignore
expect(client.headers).toHaveProperty('X-Test-Header', 'value')
// @ts-ignore
expect(client.headers).toHaveProperty('X-Client-Info')
})
})

describe('Storage Key', () => {
test('should use default storage key based on project ref', () => {
const client = createClient('https://project-ref.supabase.co', KEY)
// @ts-ignore
expect(client.storageKey).toBe('sb-project-ref-auth-token')
})

test('should use custom storage key when provided', () => {
const customStorageKey = 'custom-storage-key'
const client = createClient(URL, KEY, {
auth: { storageKey: customStorageKey },
})
// @ts-ignore
expect(client.storageKey).toBe(customStorageKey)
})
})

describe('Client Methods', () => {
test('should initialize functions client', () => {
const client = createClient(URL, KEY)
const functions = client.functions
expect(functions).toBeDefined()
// @ts-ignore
expect(functions.url).toBe('http://localhost:3000/functions/v1')
})

test('should initialize storage client', () => {
const client = createClient(URL, KEY)
const storage = client.storage
expect(storage).toBeDefined()
// @ts-ignore
expect(storage.url).toBe('http://localhost:3000/storage/v1')
})

test('should initialize realtime client', () => {
const client = createClient(URL, KEY)
expect(client.realtime).toBeDefined()
// @ts-ignore
expect(client.realtime.endPoint).toBe('ws://localhost:3000/realtime/v1/websocket')
})
})

describe('Realtime Channel Management', () => {
test('should create and manage channels', () => {
const client = createClient(URL, KEY)
const channel = client.channel('test-channel')
expect(channel).toBeDefined()
expect(client.getChannels()).toHaveLength(1)
})

test('should remove channel', async () => {
const client = createClient(URL, KEY)
const channel = client.channel('test-channel')
const result = await client.removeChannel(channel)
expect(result).toBe('ok')
expect(client.getChannels()).toHaveLength(0)
})

test('should remove all channels', async () => {
const client = createClient(URL, KEY)
client.channel('channel1')
client.channel('channel2')
const results = await client.removeAllChannels()
expect(results).toEqual(['ok', 'ok'])
expect(client.getChannels()).toHaveLength(0)
})
})

describe('Schema Management', () => {
test('should switch schema', () => {
const client = createClient<Database>(URL, KEY)
const schemaClient = client.schema('personal')
expect(schemaClient).toBeDefined()
expect(schemaClient).toBeInstanceOf(PostgrestClient)
})
})

describe('RPC Calls', () => {
test('should make RPC call with arguments', () => {
const client = createClient<Database>(URL, KEY)
const rpcCall = client.rpc('get_status', { name_param: 'test' })
expect(rpcCall).toBeDefined()
})

test('should make RPC call with options', () => {
const client = createClient<Database>(URL, KEY)
const rpcCall = client.rpc('get_status', { name_param: 'test' }, { head: true })
expect(rpcCall).toBeDefined()
})
})
})
86 changes: 0 additions & 86 deletions test/client.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion test/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test('override setting defaults', async () => {
expect(settings.auth.autoRefreshToken).toBe(autoRefreshOption)
// Existing default properties should not be overwritten
expect(settings.auth.persistSession).not.toBeNull()
expect(settings.global.headers).toBe(DEFAULT_HEADERS)
expect(settings.global.headers).toStrictEqual(DEFAULT_HEADERS)
// Existing property values should remain constant
expect(settings.db.schema).toBe(defaults.db.schema)
})