Skip to content

Commit 14e02df

Browse files
committed
feat: add auto export here
1 parent e3dac1c commit 14e02df

File tree

2 files changed

+364
-2
lines changed

2 files changed

+364
-2
lines changed

src/SupabaseClient.auto.ts

Lines changed: 362 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,362 @@
1+
/**
2+
* SupabaseClient with auto-detection for WebSocket
3+
* This version imports from @supabase/realtime-js/auto which includes dynamic imports
4+
* for backward compatibility with Node.js < 22
5+
*
6+
* @deprecated Will be removed in v3.0.0
7+
*/
8+
9+
import { FunctionsClient } from '@supabase/functions-js'
10+
import { AuthChangeEvent } from '@supabase/auth-js'
11+
import {
12+
PostgrestClient,
13+
PostgrestFilterBuilder,
14+
PostgrestQueryBuilder,
15+
} from '@supabase/postgrest-js'
16+
import {
17+
RealtimeChannel,
18+
RealtimeChannelOptions,
19+
RealtimeClient,
20+
RealtimeClientOptions,
21+
// Import from realtime-js/auto instead of main export
22+
// @ts-ignore - TypeScript doesn't understand package.json exports with Node moduleResolution
23+
} from '@supabase/realtime-js/auto'
24+
import { StorageClient as SupabaseStorageClient } from '@supabase/storage-js'
25+
import {
26+
DEFAULT_GLOBAL_OPTIONS,
27+
DEFAULT_DB_OPTIONS,
28+
DEFAULT_AUTH_OPTIONS,
29+
DEFAULT_REALTIME_OPTIONS,
30+
} from './lib/constants'
31+
import { fetchWithAuth } from './lib/fetch'
32+
import { ensureTrailingSlash, applySettingDefaults } from './lib/helpers'
33+
import { SupabaseAuthClient } from './lib/SupabaseAuthClient'
34+
import { Fetch, GenericSchema, SupabaseClientOptions, SupabaseAuthClientOptions } from './lib/types'
35+
36+
/**
37+
* Supabase Client.
38+
*
39+
* An isomorphic Javascript client for interacting with Postgres.
40+
*/
41+
export default class SupabaseClient<
42+
Database = any,
43+
SchemaName extends string & keyof Database = 'public' extends keyof Database
44+
? 'public'
45+
: string & keyof Database,
46+
Schema extends GenericSchema = Database[SchemaName] extends GenericSchema
47+
? Database[SchemaName]
48+
: any
49+
> {
50+
/**
51+
* Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies.
52+
*/
53+
auth: SupabaseAuthClient
54+
realtime: RealtimeClient
55+
/**
56+
* Supabase Storage allows you to manage user-generated content, such as photos or videos.
57+
*/
58+
storage: SupabaseStorageClient
59+
60+
protected realtimeUrl: URL
61+
protected authUrl: URL
62+
protected storageUrl: URL
63+
protected functionsUrl: URL
64+
protected rest: PostgrestClient<Database, SchemaName, Schema>
65+
protected storageKey: string
66+
protected fetch?: Fetch
67+
protected changedAccessToken?: string
68+
protected accessToken?: () => Promise<string | null>
69+
70+
protected headers: Record<string, string>
71+
72+
/**
73+
* Create a new client for use in the browser.
74+
* @param supabaseUrl The unique Supabase URL which is supplied when you create a new project in your project dashboard.
75+
* @param supabaseKey The unique Supabase Key which is supplied when you create a new project in your project dashboard.
76+
* @param options.db.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase.
77+
* @param options.auth.autoRefreshToken Set to "true" if you want to automatically refresh the token before expiring.
78+
* @param options.auth.persistSession Set to "true" if you want to automatically save the user session into local storage.
79+
* @param options.auth.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user.
80+
* @param options.realtime Options passed along to realtime-js constructor.
81+
* @param options.storage Options passed along to the storage-js constructor.
82+
* @param options.global.fetch A custom fetch implementation.
83+
* @param options.global.headers Any additional headers to send with each network request.
84+
*/
85+
constructor(
86+
protected supabaseUrl: string,
87+
protected supabaseKey: string,
88+
options?: SupabaseClientOptions<SchemaName>
89+
) {
90+
if (!supabaseUrl) throw new Error('supabaseUrl is required.')
91+
if (!supabaseKey) throw new Error('supabaseKey is required.')
92+
93+
const _supabaseUrl = ensureTrailingSlash(supabaseUrl)
94+
const baseUrl = new URL(_supabaseUrl)
95+
96+
this.realtimeUrl = new URL('realtime/v1', baseUrl)
97+
this.realtimeUrl.protocol = this.realtimeUrl.protocol.replace('http', 'ws')
98+
this.authUrl = new URL('auth/v1', baseUrl)
99+
this.storageUrl = new URL('storage/v1', baseUrl)
100+
this.functionsUrl = new URL('functions/v1', baseUrl)
101+
102+
// default storage key uses the supabase project ref as a namespace
103+
const defaultStorageKey = `sb-${baseUrl.hostname.split('.')[0]}-auth-token`
104+
const DEFAULTS = {
105+
db: DEFAULT_DB_OPTIONS,
106+
realtime: DEFAULT_REALTIME_OPTIONS,
107+
auth: { ...DEFAULT_AUTH_OPTIONS, storageKey: defaultStorageKey },
108+
global: DEFAULT_GLOBAL_OPTIONS,
109+
}
110+
111+
const settings = applySettingDefaults(options ?? {}, DEFAULTS)
112+
113+
this.storageKey = settings.auth.storageKey ?? ''
114+
this.headers = settings.global.headers ?? {}
115+
116+
if (!settings.accessToken) {
117+
this.auth = this._initSupabaseAuthClient(
118+
settings.auth ?? {},
119+
this.headers,
120+
settings.global.fetch
121+
)
122+
} else {
123+
this.accessToken = settings.accessToken
124+
125+
this.auth = new Proxy<SupabaseAuthClient>({} as any, {
126+
get: (_, prop) => {
127+
throw new Error(
128+
`@supabase/supabase-js: Supabase Client is configured with the accessToken option, accessing supabase.auth.${String(
129+
prop
130+
)} is not possible`
131+
)
132+
},
133+
})
134+
}
135+
136+
this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global.fetch)
137+
this.realtime = this._initRealtimeClient({
138+
headers: this.headers,
139+
accessToken: this._getAccessToken.bind(this),
140+
...settings.realtime,
141+
})
142+
this.rest = new PostgrestClient(new URL('rest/v1', baseUrl).href, {
143+
headers: this.headers,
144+
schema: settings.db.schema,
145+
fetch: this.fetch,
146+
})
147+
148+
this.storage = new SupabaseStorageClient(
149+
this.storageUrl.href,
150+
this.headers,
151+
this.fetch,
152+
options?.storage
153+
)
154+
155+
if (!settings.accessToken) {
156+
this._listenForAuthEvents()
157+
}
158+
}
159+
160+
/**
161+
* Supabase Functions allows you to deploy and invoke edge functions.
162+
*/
163+
get functions(): FunctionsClient {
164+
return new FunctionsClient(this.functionsUrl.href, {
165+
headers: this.headers,
166+
customFetch: this.fetch,
167+
})
168+
}
169+
170+
// NOTE: signatures must be kept in sync with PostgrestClient.from
171+
from<
172+
TableName extends string & keyof Schema['Tables'],
173+
Table extends Schema['Tables'][TableName]
174+
>(relation: TableName): PostgrestQueryBuilder<Schema, Table, TableName>
175+
from<ViewName extends string & keyof Schema['Views'], View extends Schema['Views'][ViewName]>(
176+
relation: ViewName
177+
): PostgrestQueryBuilder<Schema, View, ViewName>
178+
/**
179+
* Perform a query on a table or a view.
180+
*
181+
* @param relation - The table or view name to query
182+
*/
183+
from(relation: string): PostgrestQueryBuilder<Schema, any, any> {
184+
return this.rest.from(relation)
185+
}
186+
187+
// NOTE: signatures must be kept in sync with PostgrestClient.schema
188+
/**
189+
* Select a schema to query or perform an function (rpc) call.
190+
*
191+
* The schema needs to be on the list of exposed schemas inside Supabase.
192+
*
193+
* @param schema - The schema to query
194+
*/
195+
schema<DynamicSchema extends string & keyof Database>(
196+
schema: DynamicSchema
197+
): PostgrestClient<
198+
Database,
199+
DynamicSchema,
200+
Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any
201+
> {
202+
return this.rest.schema<DynamicSchema>(schema)
203+
}
204+
205+
// NOTE: signatures must be kept in sync with PostgrestClient.rpc
206+
/**
207+
* Perform a function call.
208+
*
209+
* @param fn - The function name to call
210+
* @param args - The arguments to pass to the function call
211+
* @param options - Named parameters
212+
* @param options.head - When set to `true`, `data` will not be returned.
213+
* Useful if you only need the count.
214+
* @param options.get - When set to `true`, the function will be called with
215+
* read-only access mode.
216+
* @param options.count - Count algorithm to use to count rows returned by the
217+
* function. Only applicable for [set-returning
218+
* functions](https://www.postgresql.org/docs/current/functions-srf.html).
219+
*
220+
* `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the
221+
* hood.
222+
*
223+
* `"planned"`: Approximated but fast count algorithm. Uses the Postgres
224+
* statistics under the hood.
225+
*
226+
* `"estimated"`: Uses exact count for low numbers and planned count for high
227+
* numbers.
228+
*/
229+
rpc<FnName extends string & keyof Schema['Functions'], Fn extends Schema['Functions'][FnName]>(
230+
fn: FnName,
231+
args: Fn['Args'] = {},
232+
options: {
233+
head?: boolean
234+
get?: boolean
235+
count?: 'exact' | 'planned' | 'estimated'
236+
} = {}
237+
): PostgrestFilterBuilder<
238+
Schema,
239+
Fn['Returns'] extends any[]
240+
? Fn['Returns'][number] extends Record<string, unknown>
241+
? Fn['Returns'][number]
242+
: never
243+
: never,
244+
Fn['Returns'],
245+
FnName,
246+
null
247+
> {
248+
return this.rest.rpc(fn, args, options)
249+
}
250+
251+
/**
252+
* Creates a Realtime channel with Broadcast, Presence, and Postgres Changes.
253+
*
254+
* @param {string} name - The name of the Realtime channel.
255+
* @param {Object} opts - The options to pass to the Realtime channel.
256+
*
257+
*/
258+
channel(name: string, opts: RealtimeChannelOptions = { config: {} }): RealtimeChannel {
259+
return this.realtime.channel(name, opts)
260+
}
261+
262+
/**
263+
* Returns all Realtime channels.
264+
*/
265+
getChannels(): RealtimeChannel[] {
266+
return this.realtime.getChannels()
267+
}
268+
269+
/**
270+
* Unsubscribes and removes Realtime channel from Realtime client.
271+
*
272+
* @param {RealtimeChannel} channel - The name of the Realtime channel.
273+
*
274+
*/
275+
removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> {
276+
return this.realtime.removeChannel(channel)
277+
}
278+
279+
/**
280+
* Unsubscribes and removes all Realtime channels from Realtime client.
281+
*/
282+
removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> {
283+
return this.realtime.removeAllChannels()
284+
}
285+
286+
private async _getAccessToken() {
287+
if (this.accessToken) {
288+
return await this.accessToken()
289+
}
290+
291+
const { data } = await this.auth.getSession()
292+
293+
return data.session?.access_token ?? this.supabaseKey
294+
}
295+
296+
private _initSupabaseAuthClient(
297+
{
298+
autoRefreshToken,
299+
persistSession,
300+
detectSessionInUrl,
301+
storage,
302+
storageKey,
303+
flowType,
304+
lock,
305+
debug,
306+
}: SupabaseAuthClientOptions,
307+
headers?: Record<string, string>,
308+
fetch?: Fetch
309+
) {
310+
const authHeaders = {
311+
Authorization: `Bearer ${this.supabaseKey}`,
312+
apikey: `${this.supabaseKey}`,
313+
}
314+
return new SupabaseAuthClient({
315+
url: this.authUrl.href,
316+
headers: { ...authHeaders, ...headers },
317+
storageKey: storageKey,
318+
autoRefreshToken,
319+
persistSession,
320+
detectSessionInUrl,
321+
storage,
322+
flowType,
323+
lock,
324+
debug,
325+
fetch,
326+
// auth checks if there is a custom authorizaiton header using this flag
327+
// so it knows whether to return an error when getUser is called with no session
328+
hasCustomAuthorizationHeader: 'Authorization' in this.headers,
329+
})
330+
}
331+
332+
private _initRealtimeClient(options: RealtimeClientOptions) {
333+
return new RealtimeClient(this.realtimeUrl.href, {
334+
...options,
335+
params: { ...{ apikey: this.supabaseKey }, ...options?.params },
336+
})
337+
}
338+
339+
private _listenForAuthEvents() {
340+
let data = this.auth.onAuthStateChange((event, session) => {
341+
this._handleTokenChanged(event, 'CLIENT', session?.access_token)
342+
})
343+
return data
344+
}
345+
346+
private _handleTokenChanged(
347+
event: AuthChangeEvent,
348+
source: 'CLIENT' | 'STORAGE',
349+
token?: string
350+
) {
351+
if (
352+
(event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') &&
353+
this.changedAccessToken !== token
354+
) {
355+
this.changedAccessToken = token
356+
} else if (event === 'SIGNED_OUT') {
357+
this.realtime.setAuth()
358+
if (source == 'STORAGE') this.auth.signOut()
359+
this.changedAccessToken = undefined
360+
}
361+
}
362+
}

src/index.auto.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* import { createClient } from '@supabase/supabase-js'
99
*/
1010

11-
import SupabaseClient from './SupabaseClient'
11+
import SupabaseClient from './SupabaseClient.auto'
1212
import type { GenericSchema, SupabaseClientOptions } from './lib/types'
1313

1414
export * from '@supabase/auth-js'
@@ -32,7 +32,7 @@ export {
3232
// @ts-ignore - TypeScript doesn't understand package.json exports with Node moduleResolution
3333
export * from '@supabase/realtime-js/auto'
3434

35-
export { default as SupabaseClient } from './SupabaseClient'
35+
export { default as SupabaseClient } from './SupabaseClient.auto'
3636
export type { SupabaseClientOptions, QueryResult, QueryData, QueryError } from './lib/types'
3737

3838
/**

0 commit comments

Comments
 (0)