import { DEFAULT_HEADERS, STORAGE_KEY } from './lib/constants' import { stripTrailingSlash, isBrowser } from './lib/helpers' import { Fetch, GenericObject, SupabaseClientOptions } from './lib/types' import { SupabaseAuthClient } from './lib/SupabaseAuthClient' import { SupabaseQueryBuilder } from './lib/SupabaseQueryBuilder' import { SupabaseStorageClient } from './storage-js/src/index' import { FunctionsClient } from './functions-js/src/index' import { PostgrestClient } from './postgrest-js/src/index' import { AuthChangeEvent } from './gotrue-js/src/index' import { RealtimeChannel, RealtimeClient, RealtimeClientOptions } from './realtime-js/src/index' const DEFAULT_OPTIONS = { schema: 'public', autoRefreshToken: true, persistSession: true, detectSessionInUrl: true, multiTab: true, headers: DEFAULT_HEADERS, } const DEFAULT_REALTIME_OPTIONS: RealtimeClientOptions = {} /** * Supabase Client. * * An isomorphic Javascript client for interacting with Postgres. */ export default class SupabaseClient { /** * Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. */ auth: SupabaseAuthClient protected realtimeUrl: string protected schema: string protected restUrl: string protected authUrl: string protected storageUrl: string protected functionsUrl: string protected multiTab: boolean protected realtime: RealtimeClient protected fetch?: Fetch protected changedAccessToken: string | undefined protected shouldThrowOnError: boolean protected headers: { [key: string]: string } /** * Create a new client for use in the browser. * @param supabaseUrl The unique Supabase URL which is supplied when you create a new project in your project dashboard. * @param supabaseKey The unique Supabase Key which is supplied when you create a new project in your project dashboard. * @param options.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase. * @param options.autoRefreshToken Set to "true" if you want to automatically refresh the token before expiring. * @param options.persistSession Set to "true" if you want to automatically save the user session into local storage. * @param options.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user. * @param options.headers Any additional headers to send with each network request. * @param options.multiTab Set to "false" if you want to disable multi-tab/window events. * @param options.fetch A custom fetch implementation. */ constructor( protected supabaseUrl: string, protected supabaseKey: string, options?: SupabaseClientOptions ) { if (!supabaseUrl) throw new Error('supabaseUrl is required.') if (!supabaseKey) throw new Error('supabaseKey is required.') const _supabaseUrl = stripTrailingSlash(supabaseUrl) const settings = { ...DEFAULT_OPTIONS, ...options } this.realtimeUrl = `${_supabaseUrl}/realtime/v1`.replace(/^http/i, 'ws') this.restUrl = `${_supabaseUrl}/rest/v1` this.authUrl = `${_supabaseUrl}/auth/v1` this.storageUrl = `${_supabaseUrl}/storage/v1` const isPlatform = _supabaseUrl.match(/(supabase\.co)|(supabase\.in)/) if (isPlatform) { const urlParts = _supabaseUrl.split('.') this.functionsUrl = `${urlParts[0]}.functions.${urlParts[1]}.${urlParts[2]}` } else { this.functionsUrl = `${_supabaseUrl}/functions/v1` } this.schema = settings.schema this.multiTab = settings.multiTab this.fetch = settings.fetch this.headers = { ...DEFAULT_HEADERS, ...options?.headers } this.realtime = this._initRealtimeClient({ headers: this.headers, ...settings.realtime }) this.shouldThrowOnError = settings.shouldThrowOnError || false this.auth = this._initSupabaseAuthClient(settings) this._listenForAuthEvents() this._listenForMultiTabEvents() } /** * Supabase Functions allows you to deploy and invoke edge functions. */ get functions() { return new FunctionsClient(this.functionsUrl, { headers: this._getAuthHeaders(), customFetch: this.fetch, }) } /** * Supabase Storage allows you to manage user-generated content, such as photos or videos. */ get storage() { return new SupabaseStorageClient(this.storageUrl, this._getAuthHeaders(), this.fetch) } /** * Perform a table operation. * * @param table The table name to operate on. */ from(table: string): SupabaseQueryBuilder { const url = `${this.restUrl}/${table}` return new SupabaseQueryBuilder(url, { headers: this._getAuthHeaders(), schema: this.schema, table, fetch: this.fetch, shouldThrowOnError: this.shouldThrowOnError, }) } /** * Perform a function call. * * @param fn The function name to call. * @param params The parameters to pass to the function call. * @param head When set to true, no data will be returned. * @param count Count algorithm to use to count rows in a table. * */ rpc( fn: string, params?: object, { head = false, count = null, }: { head?: boolean; count?: null | 'exact' | 'planned' | 'estimated' } = {} ) { const rest = this._initPostgRESTClient() return rest.rpc(fn, params, { head, count }) } /** * Creates a Realtime channel with Broadcast, Presence, and Postgres Changes. * * @param {string} name - The name of the Realtime channel. * @param {Object} opts - The options to pass to the Realtime channel. * */ channel(name: string, opts: any = {}): RealtimeChannel { return this.realtime.channel(name, opts) } /** * Returns all Realtime channels. */ getChannels(): RealtimeChannel[] { return this.realtime.getChannels() } /** * Unsubscribes and removes Realtime channel from Realtime client. * * @param {RealtimeChannel} channel - The name of the Realtime channel. * */ removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> { return this.realtime.removeChannel(channel) } /** * Unsubscribes and removes all Realtime channels from Realtime client. */ removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> { return this.realtime.removeAllChannels() } private _initRealtimeClient(options: RealtimeClientOptions) { return new RealtimeClient(this.realtimeUrl, { ...options, params: { ...{ apikey: this.supabaseKey }, ...options?.params }, }) } private _initSupabaseAuthClient({ autoRefreshToken, persistSession, detectSessionInUrl, localStorage, headers, fetch, cookieOptions, multiTab, }: SupabaseClientOptions) { const authHeaders = { Authorization: `Bearer ${this.supabaseKey}`, apikey: `${this.supabaseKey}`, } return new SupabaseAuthClient({ url: this.authUrl, headers: { ...headers, ...authHeaders }, autoRefreshToken, persistSession, detectSessionInUrl, localStorage, fetch, cookieOptions, multiTab, }) } private _initPostgRESTClient() { return new PostgrestClient(this.restUrl, { headers: this._getAuthHeaders(), schema: this.schema, fetch: this.fetch, throwOnError: this.shouldThrowOnError, }) } private _getAuthHeaders(): GenericObject { const headers: GenericObject = { ...this.headers } const authBearer = this.auth.session()?.access_token ?? this.supabaseKey headers['apikey'] = this.supabaseKey headers['Authorization'] = headers['Authorization'] || `Bearer ${authBearer}` return headers } private _listenForMultiTabEvents() { if (!this.multiTab || !isBrowser() || !window?.addEventListener) { return null } try { return window?.addEventListener('storage', (e: StorageEvent) => { if (e.key === STORAGE_KEY) { const newSession = JSON.parse(String(e.newValue)) const accessToken: string | undefined = newSession?.currentSession?.access_token ?? undefined const previousAccessToken = this.auth.session()?.access_token if (!accessToken) { this._handleTokenChanged('SIGNED_OUT', accessToken, 'STORAGE') } else if (!previousAccessToken && accessToken) { this._handleTokenChanged('SIGNED_IN', accessToken, 'STORAGE') } else if (previousAccessToken !== accessToken) { this._handleTokenChanged('TOKEN_REFRESHED', accessToken, 'STORAGE') } } }) } catch (error) { console.error('_listenForMultiTabEvents', error) return null } } private _listenForAuthEvents() { let { data } = this.auth.onAuthStateChange((event, session) => { this._handleTokenChanged(event, session?.access_token, 'CLIENT') }) return data } private _handleTokenChanged( event: AuthChangeEvent, token: string | undefined, source: 'CLIENT' | 'STORAGE' ) { if ( (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') && this.changedAccessToken !== token ) { // Token has changed // Ideally we should call this.auth.recoverSession() - need to make public // to trigger a "SIGNED_IN" event on this client. if (source == 'STORAGE') this.auth.setAuth(token!) this.changedAccessToken = token } else if (event === 'SIGNED_OUT' || event === 'USER_DELETED') { // Token is removed if (source == 'STORAGE') this.auth.signOut() } } }