• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

supabase / supabase-js / 16167398859

09 Jul 2025 10:53AM UTC coverage: 72.889%. Remained the same
16167398859

Pull #1481

github

web-flow
Merge 1cb2e824d into 4505a9be9
Pull Request #1481: fix(types): restore explicit return type

55 of 93 branches covered (59.14%)

Branch coverage included in aggregate %.

109 of 132 relevant lines covered (82.58%)

26.45 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.07
/src/SupabaseClient.ts
1
import { FunctionsClient } from '@supabase/functions-js'
6✔
2
import { AuthChangeEvent } from '@supabase/auth-js'
3
import {
6✔
4
  PostgrestClient,
5
  PostgrestFilterBuilder,
6
  PostgrestQueryBuilder,
7
  ClientServerOptions as PostgrestClientServerOption,
8
  GetGenericDatabaseWithOptions,
9
} from '@supabase/postgrest-js'
10
import {
6✔
11
  RealtimeChannel,
12
  RealtimeChannelOptions,
13
  RealtimeClient,
14
  RealtimeClientOptions,
15
} from '@supabase/realtime-js'
16
import { StorageClient as SupabaseStorageClient } from '@supabase/storage-js'
6✔
17
import {
6✔
18
  DEFAULT_GLOBAL_OPTIONS,
19
  DEFAULT_DB_OPTIONS,
20
  DEFAULT_AUTH_OPTIONS,
21
  DEFAULT_REALTIME_OPTIONS,
22
} from './lib/constants'
23
import { fetchWithAuth } from './lib/fetch'
6✔
24
import { ensureTrailingSlash, applySettingDefaults } from './lib/helpers'
6✔
25
import { SupabaseAuthClient } from './lib/SupabaseAuthClient'
6✔
26
import { Fetch, GenericSchema, SupabaseClientOptions, SupabaseAuthClientOptions } from './lib/types'
27

28
/**
29
 * Supabase Client.
30
 *
31
 * An isomorphic Javascript client for interacting with Postgres.
32
 */
33

34
export type ServicesOptions = PostgrestClientServerOption & {}
35

36
export default class SupabaseClient<
6✔
37
  Database = any,
38
  ClientOptions extends ServicesOptions = { PostgrestVersion: '12' },
39
  SchemaName extends string &
40
    keyof GetGenericDatabaseWithOptions<Database>['db'] = 'public' extends keyof GetGenericDatabaseWithOptions<Database>['db']
41
    ? 'public'
42
    : string & keyof GetGenericDatabaseWithOptions<Database>['db'],
43
  Schema extends GenericSchema = GetGenericDatabaseWithOptions<Database>['db'][SchemaName] extends GenericSchema
44
    ? GetGenericDatabaseWithOptions<Database>['db'][SchemaName]
45
    : any
46
> {
47
  /**
48
   * Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
49
   */
50
  auth: SupabaseAuthClient
51
  realtime: RealtimeClient
52

53
  protected realtimeUrl: URL
54
  protected authUrl: URL
55
  protected storageUrl: URL
56
  protected functionsUrl: URL
57
  protected rest: PostgrestClient<Database, ClientOptions, SchemaName, Schema>
58
  protected storageKey: string
59
  protected fetch?: Fetch
60
  protected changedAccessToken?: string
61
  protected accessToken?: () => Promise<string | null>
62

63
  protected headers: Record<string, string>
64

65
  /**
66
   * Create a new client for use in the browser.
67
   * @param supabaseUrl The unique Supabase URL which is supplied when you create a new project in your project dashboard.
68
   * @param supabaseKey The unique Supabase Key which is supplied when you create a new project in your project dashboard.
69
   * @param options.db.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase.
70
   * @param options.auth.autoRefreshToken Set to "true" if you want to automatically refresh the token before expiring.
71
   * @param options.auth.persistSession Set to "true" if you want to automatically save the user session into local storage.
72
   * @param options.auth.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user.
73
   * @param options.realtime Options passed along to realtime-js constructor.
74
   * @param options.global.fetch A custom fetch implementation.
75
   * @param options.global.headers Any additional headers to send with each network request.
76
   */
77
  constructor(
78
    protected supabaseUrl: string,
63✔
79
    protected supabaseKey: string,
63✔
80
    options?: SupabaseClientOptions<SchemaName>
81
  ) {
82
    if (!supabaseUrl) throw new Error('supabaseUrl is required.')
63✔
83
    if (!supabaseKey) throw new Error('supabaseKey is required.')
60✔
84

85
    const _supabaseUrl = ensureTrailingSlash(supabaseUrl)
57✔
86
    const baseUrl = new URL(_supabaseUrl)
57✔
87

88
    this.realtimeUrl = new URL('realtime/v1', baseUrl)
57✔
89
    this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws')
57✔
90
    this.authUrl = new URL('auth/v1', baseUrl)
57✔
91
    this.storageUrl = new URL('storage/v1', baseUrl)
57✔
92
    this.functionsUrl = new URL('functions/v1', baseUrl)
57✔
93

94
    // default storage key uses the supabase project ref as a namespace
95
    const defaultStorageKey = `sb-${baseUrl.hostname.split('.')[0]}-auth-token`
57✔
96
    const DEFAULTS = {
57✔
97
      db: DEFAULT_DB_OPTIONS,
98
      realtime: DEFAULT_REALTIME_OPTIONS,
99
      auth: { ...DEFAULT_AUTH_OPTIONS, storageKey: defaultStorageKey },
100
      global: DEFAULT_GLOBAL_OPTIONS,
101
    }
102

103
    const settings = applySettingDefaults(options ?? {}, DEFAULTS)
57✔
104

105
    this.storageKey = settings.auth.storageKey ?? ''
57!
106
    this.headers = settings.global.headers ?? {}
57!
107

108
    if (!settings.accessToken) {
57✔
109
      this.auth = this._initSupabaseAuthClient(
54✔
110
        settings.auth ?? {},
162!
111
        this.headers,
112
        settings.global.fetch
113
      )
114
    } else {
115
      this.accessToken = settings.accessToken
3✔
116

117
      this.auth = new Proxy<SupabaseAuthClient>({} as any, {
3✔
118
        get: (_, prop) => {
119
          throw new Error(
3✔
120
            `@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(
121
              prop
122
            )} is not possible`
123
          )
124
        },
125
      })
126
    }
127

128
    this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch)
57✔
129
    this.realtime = this._initRealtimeClient({
57✔
130
      headers: this.headers,
131
      accessToken: this._getAccessToken.bind(this),
132
      ...settings.realtime,
133
    })
134
    this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {
57✔
135
      headers: this.headers,
136
      schema: settings.db.schema,
137
      fetch: this.fetch,
138
    })
139

140
    if (!settings.accessToken) {
57✔
141
      this._listenForAuthEvents()
54✔
142
    }
143
  }
144

145
  /**
146
   * Supabase Functions allows you to deploy and invoke edge functions.
147
   */
148
  get functions(): FunctionsClient {
149
    return new FunctionsClient(this.functionsUrl.href, {
3✔
150
      headers: this.headers,
151
      customFetch: this.fetch,
152
    })
153
  }
154

155
  /**
156
   * Supabase Storage allows you to manage user-generated content, such as photos or videos.
157
   */
158
  get storage(): SupabaseStorageClient {
159
    return new SupabaseStorageClient(this.storageUrl.href, this.headers, this.fetch)
3✔
160
  }
161

162
  // NOTE: signatures must be kept in sync with PostgrestClient.from
163
  from<
164
    TableName extends string & keyof Schema['Tables'],
165
    Table extends Schema['Tables'][TableName]
166
  >(relation: TableName): PostgrestQueryBuilder<ClientOptions, Schema, Table, TableName>
167
  from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>(
168
    relation: ViewName
169
  ): PostgrestQueryBuilder<ClientOptions, Schema, View, ViewName>
170
  /**
171
   * Perform a query on a table or a view.
172
   *
173
   * @param relation - The table or view name to query
174
   */
175
  from(relation: string): PostgrestQueryBuilder<ClientOptions, Schema, any, any> {
176
    return this.rest.from(relation)
×
177
  }
178

179
  // NOTE: signatures must be kept in sync with PostgrestClient.schema
180
  /**
181
   * Select a schema to query or perform an function (rpc) call.
182
   *
183
   * The schema needs to be on the list of exposed schemas inside Supabase.
184
   *
185
   * @param schema - The schema to query
186
   */
187
  schema<DynamicSchema extends string & keyof GetGenericDatabaseWithOptions<Database>['db']>(
188
    schema: DynamicSchema
189
  ): PostgrestClient<
190
    Database,
191
    ClientOptions,
192
    DynamicSchema,
193
    Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
194
  > {
195
    return this.rest.schema<DynamicSchema>(schema)
3✔
196
  }
197

198
  // NOTE: signatures must be kept in sync with PostgrestClient.rpc
199
  /**
200
   * Perform a function call.
201
   *
202
   * @param fn - The function name to call
203
   * @param args - The arguments to pass to the function call
204
   * @param options - Named parameters
205
   * @param options.head - When set to `true`, `data` will not be returned.
206
   * Useful if you only need the count.
207
   * @param options.get - When set to `true`, the function will be called with
208
   * read-only access mode.
209
   * @param options.count - Count algorithm to use to count rows returned by the
210
   * function. Only applicable for [set-returning
211
   * functions](https://www.postgresql.org/docs/current/functions-srf.html).
212
   *
213
   * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
214
   * hood.
215
   *
216
   * `"planned"`: Approximated but fast count algorithm. Uses the Postgres
217
   * statistics under the hood.
218
   *
219
   * `"estimated"`: Uses exact count for low numbers and planned count for high
220
   * numbers.
221
   */
222
  rpc<FnName extends string & keyof Schema['Functions'], Fn extends Schema['Functions'][FnName]>(
223
    fn: FnName,
224
    args: Fn['Args'] = {},
6✔
225
    options: {
9✔
226
      head?: boolean
227
      get?: boolean
228
      count?: 'exact' | 'planned' | 'estimated'
229
    } = {}
230
  ): PostgrestFilterBuilder<
231
    ClientOptions,
232
    Schema,
233
    Fn['Returns'] extends any[]
234
      ? Fn['Returns'][number] extends Record<string, unknown>
235
        ? Fn['Returns'][number]
236
        : never
237
      : never,
238
    Fn['Returns'],
239
    FnName,
240
    null,
241
    'RPC'
242
  > {
243
    return this.rest.rpc(fn, args, options)
12✔
244
  }
245

246
  /**
247
   * Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
248
   *
249
   * @param {string} name - The name of the Realtime channel.
250
   * @param {Object} opts - The options to pass to the Realtime channel.
251
   *
252
   */
253
  channel(name: string, opts: RealtimeChannelOptions = { config: {} }): RealtimeChannel {
12✔
254
    return this.realtime.channel(name, opts)
12✔
255
  }
256

257
  /**
258
   * Returns all Realtime channels.
259
   */
260
  getChannels(): RealtimeChannel[] {
261
    return this.realtime.getChannels()
9✔
262
  }
263

264
  /**
265
   * Unsubscribes and removes Realtime channel from Realtime client.
266
   *
267
   * @param {RealtimeChannel} channel - The name of the Realtime channel.
268
   *
269
   */
270
  removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> {
271
    return this.realtime.removeChannel(channel)
3✔
272
  }
273

274
  /**
275
   * Unsubscribes and removes all Realtime channels from Realtime client.
276
   */
277
  removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> {
278
    return this.realtime.removeAllChannels()
3✔
279
  }
280

281
  private async _getAccessToken() {
282
    if (this.accessToken) {
×
283
      return await this.accessToken()
×
284
    }
285

286
    const { data } = await this.auth.getSession()
×
287

288
    return data.session?.access_token ?? null
×
289
  }
290

291
  private _initSupabaseAuthClient(
292
    {
293
      autoRefreshToken,
294
      persistSession,
295
      detectSessionInUrl,
296
      storage,
297
      storageKey,
298
      flowType,
299
      lock,
300
      debug,
301
    }: SupabaseAuthClientOptions,
302
    headers?: Record<string, string>,
303
    fetch?: Fetch
304
  ) {
305
    const authHeaders = {
57✔
306
      Authorization: `Bearer ${this.supabaseKey}`,
307
      apikey: `${this.supabaseKey}`,
308
    }
309
    return new SupabaseAuthClient({
57✔
310
      url: this.authUrl.href,
311
      headers: { ...authHeaders, ...headers },
312
      storageKey: storageKey,
313
      autoRefreshToken,
314
      persistSession,
315
      detectSessionInUrl,
316
      storage,
317
      flowType,
318
      lock,
319
      debug,
320
      fetch,
321
      // auth checks if there is a custom authorizaiton header using this flag
322
      // so it knows whether to return an error when getUser is called with no session
323
      hasCustomAuthorizationHeader: 'Authorization' in this.headers,
324
    })
325
  }
326

327
  private _initRealtimeClient(options: RealtimeClientOptions) {
328
    return new RealtimeClient(this.realtimeUrl.href, {
57✔
329
      ...options,
330
      params: { ...{ apikey: this.supabaseKey }, ...options?.params },
171!
331
    })
332
  }
333

334
  private _listenForAuthEvents() {
335
    let data = this.auth.onAuthStateChange((event, session) => {
54✔
336
      this._handleTokenChanged(event, 'CLIENT', session?.access_token)
54!
337
    })
338
    return data
54✔
339
  }
340

341
  private _handleTokenChanged(
342
    event: AuthChangeEvent,
343
    source: 'CLIENT' | 'STORAGE',
344
    token?: string
345
  ) {
346
    if (
54!
347
      (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') &&
108!
348
      this.changedAccessToken !== token
349
    ) {
350
      this.changedAccessToken = token
×
351
    } else if (event === 'SIGNED_OUT') {
54!
352
      this.realtime.setAuth()
×
353
      if (source == 'STORAGE') this.auth.signOut()
×
354
      this.changedAccessToken = undefined
×
355
    }
356
  }
357
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc