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

supabase / supabase-js / 15138046364

20 May 2025 12:52PM UTC coverage: 69.058% (-3.6%) from 72.646%
15138046364

Pull #1418

github

web-flow
Merge 915c5d0fc into bc4bce1ca
Pull Request #1418: fix: Bump up realtime-js

48 of 91 branches covered (52.75%)

Branch coverage included in aggregate %.

6 of 8 new or added lines in 2 files covered. (75.0%)

2 existing lines in 1 file now uncovered.

106 of 132 relevant lines covered (80.3%)

8.05 hits per line

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

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

26
/**
27
 * Supabase Client.
28
 *
29
 * An isomorphic Javascript client for interacting with Postgres.
30
 */
31
export default class SupabaseClient<
2✔
32
  Database = any,
33
  SchemaName extends string & keyof Database = 'public' extends keyof Database
34
    ? 'public'
35
    : string & keyof Database,
36
  Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
37
    ? Database[SchemaName]
38
    : any
39
> {
40
  /**
41
   * Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
42
   */
43
  auth: SupabaseAuthClient
44
  realtime: RealtimeClient
45

46
  protected realtimeUrl: URL
47
  protected authUrl: URL
48
  protected storageUrl: URL
49
  protected functionsUrl: URL
50
  protected rest: PostgrestClient<Database, SchemaName, Schema>
51
  protected storageKey: string
52
  protected fetch?: Fetch
53
  protected changedAccessToken?: string
54
  protected accessToken?: () => Promise<string | null>
55

56
  protected headers: Record<string, string>
57

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

78
    const _supabaseUrl = cleanUrl(supabaseUrl)
18✔
79
    const baseUrl = new URL(_supabaseUrl)
18✔
80

81
    this.realtimeUrl = new URL('/realtime/v1', baseUrl)
18✔
82
    this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws')
18✔
83
    this.authUrl = new URL('/auth/v1', baseUrl)
18✔
84
    this.storageUrl = new URL('/storage/v1', baseUrl)
18✔
85
    this.functionsUrl = new URL('/functions/v1', baseUrl)
18✔
86

87
    // default storage key uses the supabase project ref as a namespace
88
    const defaultStorageKey = `sb-${baseUrl.hostname.split('.')[0]}-auth-token`
18✔
89
    const DEFAULTS = {
18✔
90
      db: DEFAULT_DB_OPTIONS,
91
      realtime: DEFAULT_REALTIME_OPTIONS,
92
      auth: { ...DEFAULT_AUTH_OPTIONS, storageKey: defaultStorageKey },
93
      global: DEFAULT_GLOBAL_OPTIONS,
94
    }
95

96
    const settings = applySettingDefaults(options ?? {}, DEFAULTS)
18✔
97

98
    this.storageKey = settings.auth.storageKey ?? ''
18!
99
    this.headers = settings.global.headers ?? {}
18!
100

101
    if (!settings.accessToken) {
18✔
102
      this.auth = this._initSupabaseAuthClient(
17✔
103
        settings.auth ?? {},
51!
104
        this.headers,
105
        settings.global.fetch
106
      )
107
    } else {
108
      this.accessToken = settings.accessToken
1✔
109

110
      this.auth = new Proxy<SupabaseAuthClient>({} as any, {
1✔
111
        get: (_, prop) => {
112
          throw new Error(
1✔
113
            `@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(
114
              prop
115
            )} is not possible`
116
          )
117
        },
118
      })
119
    }
120

121
    this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch)
18✔
122

123
    this.realtime = this._initRealtimeClient({
18✔
124
      headers: this.headers,
125
      accessToken: this._getAccessToken.bind(this),
126
      ...settings.realtime,
127
    })
128

129
    this.rest = new PostgrestClient(`${_supabaseUrl}/rest/v1`, {
18✔
130
      headers: this.headers,
131
      schema: settings.db.schema,
132
      fetch: this.fetch,
133
    })
134

135
    if (!settings.accessToken) {
18✔
136
      this._listenForAuthEvents()
17✔
137
    }
138
  }
139

140
  /**
141
   * Supabase Functions allows you to deploy and invoke edge functions.
142
   */
143
  get functions(): FunctionsClient {
144
    return new FunctionsClient(this.functionsUrl.href, {
1✔
145
      headers: this.headers,
146
      customFetch: this.fetch,
147
    })
148
  }
149

150
  /**
151
   * Supabase Storage allows you to manage user-generated content, such as photos or videos.
152
   */
153
  get storage(): SupabaseStorageClient {
154
    return new SupabaseStorageClient(this.storageUrl.href, this.headers, this.fetch)
1✔
155
  }
156

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

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

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

238
  /**
239
   * Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
240
   *
241
   * @param {string} name - The name of the Realtime channel.
242
   * @param {Object} opts - The options to pass to the Realtime channel.
243
   *
244
   */
245
  channel(name: string, opts: RealtimeChannelOptions = { config: {} }): RealtimeChannel {
4✔
246
    return this.realtime.channel(name, opts)
4✔
247
  }
248

249
  /**
250
   * Returns all Realtime channels.
251
   */
252
  getChannels(): RealtimeChannel[] {
253
    return this.realtime.getChannels()
3✔
254
  }
255

256
  /**
257
   * Unsubscribes and removes Realtime channel from Realtime client.
258
   *
259
   * @param {RealtimeChannel} channel - The name of the Realtime channel.
260
   *
261
   */
262
  removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> {
263
    return this.realtime.removeChannel(channel)
1✔
264
  }
265

266
  /**
267
   * Unsubscribes and removes all Realtime channels from Realtime client.
268
   */
269
  removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> {
270
    return this.realtime.removeAllChannels()
1✔
271
  }
272

273
  private async _getAccessToken() {
274
    if (this.accessToken) {
×
275
      return await this.accessToken()
×
276
    }
277

278
    const { data } = await this.auth.getSession()
×
279

280
    return data.session?.access_token ?? null
×
281
  }
282

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

319
  private _initRealtimeClient(options: RealtimeClientOptions) {
320
    return new RealtimeClient(this.realtimeUrl.href, {
18✔
321
      ...options,
322
      params: { ...{ apikey: this.supabaseKey }, ...options?.params },
54!
323
    })
324
  }
325

326
  private async _listenForAuthEvents() {
327
    return await this.auth.onAuthStateChange((event, session) => {
17✔
328
      setTimeout(
17✔
NEW
329
        async () => await this._handleTokenChanged(event, 'CLIENT', session?.access_token),
×
330
        0
331
      )
332
    })
333
  }
334

335
  private async _handleTokenChanged(
336
    event: AuthChangeEvent,
337
    source: 'CLIENT' | 'STORAGE',
338
    token?: string
339
  ) {
UNCOV
340
    if (
×
341
      (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') &&
×
342
      this.changedAccessToken !== token
343
    ) {
344
      this.changedAccessToken = token
×
UNCOV
345
    } else if (event === 'SIGNED_OUT') {
×
NEW
346
      await this.realtime.setAuth()
×
347
      if (source == 'STORAGE') this.auth.signOut()
×
348
      this.changedAccessToken = undefined
×
349
    }
350
  }
351
}
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