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

supabase / supabase-js / 12280569374

11 Dec 2024 04:22PM UTC coverage: 67.633% (-4.3%) from 71.981%
12280569374

Pull #1331

github

web-flow
Merge bbc58aed6 into 5e1e477a7
Pull Request #1331: fix: revert set auth call

41 of 79 branches covered (51.9%)

Branch coverage included in aggregate %.

2 of 3 new or added lines in 1 file covered. (66.67%)

3 existing lines in 1 file now uncovered.

99 of 128 relevant lines covered (77.34%)

4.63 hits per line

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

67.27
/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 { stripTrailingSlash, 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: string
47
  protected authUrl: string
48
  protected storageUrl: string
49
  protected functionsUrl: string
50
  protected rest: PostgrestClient<Database, SchemaName>
51
  protected storageKey: string
52
  protected fetch?: Fetch
53
  protected changedAccessToken?: string
54
  protected accessToken?: () => Promise<string>
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,
10✔
72
    protected supabaseKey: string,
10✔
73
    options?: SupabaseClientOptions<SchemaName>
74
  ) {
75
    if (!supabaseUrl) throw new Error('supabaseUrl is required.')
10✔
76
    if (!supabaseKey) throw new Error('supabaseKey is required.')
9✔
77

78
    const _supabaseUrl = stripTrailingSlash(supabaseUrl)
8✔
79

80
    this.realtimeUrl = `${_supabaseUrl}/realtime/v1`.replace(/^http/i, 'ws')
8✔
81
    this.authUrl = `${_supabaseUrl}/auth/v1`
8✔
82
    this.storageUrl = `${_supabaseUrl}/storage/v1`
8✔
83
    this.functionsUrl = `${_supabaseUrl}/functions/v1`
8✔
84

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

94
    const settings = applySettingDefaults(options ?? {}, DEFAULTS)
8✔
95

96
    this.storageKey = settings.auth.storageKey ?? ''
8!
97
    this.headers = settings.global.headers ?? {}
8!
98

99
    if (!settings.accessToken) {
8✔
100
      this.auth = this._initSupabaseAuthClient(
7✔
101
        settings.auth ?? {},
21!
102
        this.headers,
103
        settings.global.fetch
104
      )
105
    } else {
106
      this.accessToken = settings.accessToken
1✔
107

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

119
    this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch)
8✔
120
    this.realtime = this._initRealtimeClient({
8✔
121
      headers: this.headers,
122
      accessToken: this._getAccessToken.bind(this),
123
      ...settings.realtime,
124
    })
125
    this.rest = new PostgrestClient(`${_supabaseUrl}/rest/v1`, {
8✔
126
      headers: this.headers,
127
      schema: settings.db.schema,
128
      fetch: this.fetch,
129
    })
130

131
    if (!settings.accessToken) {
8✔
132
      this._listenForAuthEvents()
7✔
133
    }
134
  }
135

136
  /**
137
   * Supabase Functions allows you to deploy and invoke edge functions.
138
   */
139
  get functions(): FunctionsClient {
140
    return new FunctionsClient(this.functionsUrl, {
×
141
      headers: this.headers,
142
      customFetch: this.fetch,
143
    })
144
  }
145

146
  /**
147
   * Supabase Storage allows you to manage user-generated content, such as photos or videos.
148
   */
149
  get storage(): SupabaseStorageClient {
150
    return new SupabaseStorageClient(this.storageUrl, this.headers, this.fetch)
×
151
  }
152

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

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

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

232
  /**
233
   * Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
234
   *
235
   * @param {string} name - The name of the Realtime channel.
236
   * @param {Object} opts - The options to pass to the Realtime channel.
237
   *
238
   */
239
  channel(name: string, opts: RealtimeChannelOptions = { config: {} }): RealtimeChannel {
×
240
    return this.realtime.channel(name, opts)
×
241
  }
242

243
  /**
244
   * Returns all Realtime channels.
245
   */
246
  getChannels(): RealtimeChannel[] {
247
    return this.realtime.getChannels()
×
248
  }
249

250
  /**
251
   * Unsubscribes and removes Realtime channel from Realtime client.
252
   *
253
   * @param {RealtimeChannel} channel - The name of the Realtime channel.
254
   *
255
   */
256
  removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> {
257
    return this.realtime.removeChannel(channel)
×
258
  }
259

260
  /**
261
   * Unsubscribes and removes all Realtime channels from Realtime client.
262
   */
263
  removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> {
264
    return this.realtime.removeAllChannels()
×
265
  }
266

267
  private async _getAccessToken() {
UNCOV
268
    if (this.accessToken) {
×
269
      return await this.accessToken()
×
270
    }
271

UNCOV
272
    const { data } = await this.auth.getSession()
×
273

UNCOV
274
    return data.session?.access_token ?? null
×
275
  }
276

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

313
  private _initRealtimeClient(options: RealtimeClientOptions) {
314
    return new RealtimeClient(this.realtimeUrl, {
8✔
315
      ...options,
316
      params: { ...{ apikey: this.supabaseKey }, ...options?.params },
24!
317
    })
318
  }
319

320
  private _listenForAuthEvents() {
321
    let data = this.auth.onAuthStateChange((event, session) => {
7✔
322
      this._handleTokenChanged(event, 'CLIENT', session?.access_token)
7!
323
    })
324
    return data
7✔
325
  }
326

327
  private _handleTokenChanged(
328
    event: AuthChangeEvent,
329
    source: 'CLIENT' | 'STORAGE',
330
    token?: string
331
  ) {
332
    if (
7!
333
      (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') &&
14!
334
      this.changedAccessToken !== token
335
    ) {
336
      this.changedAccessToken = token
×
337
    } else if (event === 'SIGNED_OUT') {
7!
NEW
338
      this.realtime.setAuth()
×
339
      if (source == 'STORAGE') this.auth.signOut()
×
340
      this.changedAccessToken = undefined
×
341
    }
342
  }
343
}
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