UNPKG

@supabase/supabase-js

Version:
406 lines (377 loc) 13.7 kB
import type { AuthChangeEvent } from '@supabase/auth-js' import { FunctionsClient } from '@supabase/functions-js' import { PostgrestClient, type PostgrestFilterBuilder, type PostgrestQueryBuilder, } from '@supabase/postgrest-js' import { type RealtimeChannel, type RealtimeChannelOptions, RealtimeClient, type RealtimeClientOptions, } from '@supabase/realtime-js' import { StorageClient as SupabaseStorageClient } from '@supabase/storage-js' import { DEFAULT_AUTH_OPTIONS, DEFAULT_DB_OPTIONS, DEFAULT_GLOBAL_OPTIONS, DEFAULT_REALTIME_OPTIONS, } from './lib/constants' import { fetchWithAuth } from './lib/fetch' import { applySettingDefaults, validateSupabaseUrl } from './lib/helpers' import { SupabaseAuthClient } from './lib/SupabaseAuthClient' import type { Fetch, GenericSchema, SupabaseAuthClientOptions, SupabaseClientOptions, } from './lib/types' import { GetRpcFunctionFilterBuilderByArgs } from './lib/rest/types/common/rpc' /** * Supabase Client. * * An isomorphic Javascript client for interacting with Postgres. */ export default class SupabaseClient< Database = any, // The second type parameter is also used for specifying db_schema, so we // support both cases. // TODO: Allow setting db_schema from ClientOptions. SchemaNameOrClientOptions extends | (string & keyof Omit<Database, '__InternalSupabase'>) | { PostgrestVersion: string } = 'public' extends keyof Omit<Database, '__InternalSupabase'> ? 'public' : string & keyof Omit<Database, '__InternalSupabase'>, SchemaName extends string & keyof Omit<Database, '__InternalSupabase'> = SchemaNameOrClientOptions extends string & keyof Omit<Database, '__InternalSupabase'> ? SchemaNameOrClientOptions : 'public' extends keyof Omit<Database, '__InternalSupabase'> ? 'public' : string & keyof Omit<Omit<Database, '__InternalSupabase'>, '__InternalSupabase'>, Schema extends Omit<Database, '__InternalSupabase'>[SchemaName] extends GenericSchema ? Omit<Database, '__InternalSupabase'>[SchemaName] : never = Omit<Database, '__InternalSupabase'>[SchemaName] extends GenericSchema ? Omit<Database, '__InternalSupabase'>[SchemaName] : never, ClientOptions extends { PostgrestVersion: string } = SchemaNameOrClientOptions extends string & keyof Omit<Database, '__InternalSupabase'> ? // If the version isn't explicitly set, look for it in the __InternalSupabase object to infer the right version Database extends { __InternalSupabase: { PostgrestVersion: string } } ? Database['__InternalSupabase'] : // otherwise default to 12 { PostgrestVersion: '12' } : SchemaNameOrClientOptions extends { PostgrestVersion: string } ? SchemaNameOrClientOptions : never, > { /** * Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. */ auth: SupabaseAuthClient realtime: RealtimeClient /** * Supabase Storage allows you to manage user-generated content, such as photos or videos. */ storage: SupabaseStorageClient protected realtimeUrl: URL protected authUrl: URL protected storageUrl: URL protected functionsUrl: URL protected rest: PostgrestClient<Database, ClientOptions, SchemaName> protected storageKey: string protected fetch?: Fetch protected changedAccessToken?: string protected accessToken?: () => Promise<string | null> protected headers: Record<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.db.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase. * @param options.auth.autoRefreshToken Set to "true" if you want to automatically refresh the token before expiring. * @param options.auth.persistSession Set to "true" if you want to automatically save the user session into local storage. * @param options.auth.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user. * @param options.realtime Options passed along to realtime-js constructor. * @param options.storage Options passed along to the storage-js constructor. * @param options.global.fetch A custom fetch implementation. * @param options.global.headers Any additional headers to send with each network request. */ constructor( protected supabaseUrl: string, protected supabaseKey: string, options?: SupabaseClientOptions<SchemaName> ) { const baseUrl = validateSupabaseUrl(supabaseUrl) if (!supabaseKey) throw new Error('supabaseKey is required.') this.realtimeUrl = new URL('realtime/v1', baseUrl) this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws') 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-${baseUrl.hostname.split('.')[0]}-auth-token` const DEFAULTS = { db: DEFAULT_DB_OPTIONS, realtime: DEFAULT_REALTIME_OPTIONS, auth: { ...DEFAULT_AUTH_OPTIONS, storageKey: defaultStorageKey }, global: DEFAULT_GLOBAL_OPTIONS, } const settings = applySettingDefaults(options ?? {}, DEFAULTS) this.storageKey = settings.auth.storageKey ?? '' this.headers = settings.global.headers ?? {} if (!settings.accessToken) { this.auth = this._initSupabaseAuthClient( settings.auth ?? {}, this.headers, settings.global.fetch ) } else { this.accessToken = settings.accessToken this.auth = new Proxy<SupabaseAuthClient>({} as any, { get: (_, prop) => { throw new Error( `@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String( prop )} is not possible` ) }, }) } this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch) this.realtime = this._initRealtimeClient({ headers: this.headers, accessToken: this._getAccessToken.bind(this), ...settings.realtime, }) this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, { headers: this.headers, schema: settings.db.schema, fetch: this.fetch, }) this.storage = new SupabaseStorageClient( this.storageUrl.href, this.headers, this.fetch, options?.storage ) if (!settings.accessToken) { this._listenForAuthEvents() } } /** * Supabase Functions allows you to deploy and invoke edge functions. */ get functions(): FunctionsClient { return new FunctionsClient(this.functionsUrl.href, { headers: this.headers, customFetch: this.fetch, }) } // NOTE: signatures must be kept in sync with PostgrestClient.from from< TableName extends string & keyof Schema['Tables'], Table extends Schema['Tables'][TableName], >(relation: TableName): PostgrestQueryBuilder<ClientOptions, Schema, Table, TableName> from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>( relation: ViewName ): PostgrestQueryBuilder<ClientOptions, Schema, View, ViewName> /** * Perform a query on a table or a view. * * @param relation - The table or view name to query */ from(relation: string): PostgrestQueryBuilder<ClientOptions, Schema, any> { return this.rest.from(relation) } // NOTE: signatures must be kept in sync with PostgrestClient.schema /** * Select a schema to query or perform an function (rpc) call. * * The schema needs to be on the list of exposed schemas inside Supabase. * * @param schema - The schema to query */ schema<DynamicSchema extends string & keyof Omit<Database, '__InternalSupabase'>>( schema: DynamicSchema ): PostgrestClient< Database, ClientOptions, DynamicSchema, Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any > { return this.rest.schema<DynamicSchema>(schema) } // NOTE: signatures must be kept in sync with PostgrestClient.rpc /** * Perform a function call. * * @param fn - The function name to call * @param args - The arguments to pass to the function call * @param options - Named parameters * @param options.head - When set to `true`, `data` will not be returned. * Useful if you only need the count. * @param options.get - When set to `true`, the function will be called with * read-only access mode. * @param options.count - Count algorithm to use to count rows returned by the * function. Only applicable for [set-returning * functions](https://www.postgresql.org/docs/current/functions-srf.html). * * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the * hood. * * `"planned"`: Approximated but fast count algorithm. Uses the Postgres * statistics under the hood. * * `"estimated"`: Uses exact count for low numbers and planned count for high * numbers. */ rpc< FnName extends string & keyof Schema['Functions'], Args extends Schema['Functions'][FnName]['Args'] = never, FilterBuilder extends GetRpcFunctionFilterBuilderByArgs< Schema, FnName, Args > = GetRpcFunctionFilterBuilderByArgs<Schema, FnName, Args>, >( fn: FnName, args: Args = {} as Args, options: { head?: boolean get?: boolean count?: 'exact' | 'planned' | 'estimated' } = { head: false, get: false, count: undefined, } ): PostgrestFilterBuilder< ClientOptions, Schema, FilterBuilder['Row'], FilterBuilder['Result'], FilterBuilder['RelationName'], FilterBuilder['Relationships'], 'RPC' > { return this.rest.rpc(fn, args, options) as unknown as PostgrestFilterBuilder< ClientOptions, Schema, FilterBuilder['Row'], FilterBuilder['Result'], FilterBuilder['RelationName'], FilterBuilder['Relationships'], 'RPC' > } /** * 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: RealtimeChannelOptions = { config: {} }): 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 async _getAccessToken() { if (this.accessToken) { return await this.accessToken() } const { data } = await this.auth.getSession() return data.session?.access_token ?? this.supabaseKey } private _initSupabaseAuthClient( { autoRefreshToken, persistSession, detectSessionInUrl, storage, userStorage, storageKey, flowType, lock, debug, throwOnError, }: SupabaseAuthClientOptions, headers?: Record<string, string>, fetch?: Fetch ) { const authHeaders = { Authorization: `Bearer ${this.supabaseKey}`, apikey: `${this.supabaseKey}`, } return new SupabaseAuthClient({ url: this.authUrl.href, headers: { ...authHeaders, ...headers }, storageKey: storageKey, autoRefreshToken, persistSession, detectSessionInUrl, storage, userStorage, flowType, lock, debug, throwOnError, fetch, // auth checks if there is a custom authorizaiton header using this flag // so it knows whether to return an error when getUser is called with no session hasCustomAuthorizationHeader: Object.keys(this.headers).some( (key) => key.toLowerCase() === 'authorization' ), }) } private _initRealtimeClient(options: RealtimeClientOptions) { return new RealtimeClient(this.realtimeUrl.href, { ...options, params: { ...{ apikey: this.supabaseKey }, ...options?.params }, }) } private _listenForAuthEvents() { const data = this.auth.onAuthStateChange((event, session) => { this._handleTokenChanged(event, 'CLIENT', session?.access_token) }) return data } private _handleTokenChanged( event: AuthChangeEvent, source: 'CLIENT' | 'STORAGE', token?: string ) { if ( (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') && this.changedAccessToken !== token ) { this.changedAccessToken = token this.realtime.setAuth(token) } else if (event === 'SIGNED_OUT') { this.realtime.setAuth() if (source == 'STORAGE') this.auth.signOut() this.changedAccessToken = undefined } } }