import { FunctionsClient } from 'pasubot-functions-js' import { AuthChangeEvent } from 'pasubot-auth-js' // import { // PostgrestClient, // PostgrestFilterBuilder, // PostgrestQueryBuilder, // } from 'pasubot-postgrest-js' import { RealtimeChannel, RealtimeChannelOptions, RealtimeClient, RealtimeClientOptions, } from 'pasubot-realtime-js' import { StorageClient as PasubotStorageClient } from 'pasubot-storage-js' import { DEFAULT_GLOBAL_OPTIONS, DEFAULT_DB_OPTIONS, DEFAULT_AUTH_OPTIONS, DEFAULT_REALTIME_OPTIONS, } from './lib/constants' import { fetchWithAuth } from './lib/fetch' import { stripTrailingSlash, applySettingDefaults } from './lib/helpers' import { PasubotAuthClient } from './lib/PasubotAuthClient' import { Fetch, GenericSchema, PasubotClientOptions, PasubotAuthClientOptions } from './lib/types' import MongoDBClient from './lib/mongoDB/MongoDBClient' import MongoDBQueryBuilder from './lib/mongoDB/MongoDBQueryBuilder' import MongoDBFilterBuilder from './lib/mongoDB/MongoDBFilterBuilder' /** * Pasubot Client. * * An isomorphic Javascript client for interacting with Postgres. */ export default class PasubotClientt< Database = any, SchemaName extends string & keyof Database = 'public' extends keyof Database ? 'public' : string & keyof Database, Schema extends GenericSchema = Database[SchemaName] extends GenericSchema ? Database[SchemaName] : any > { /** * Pasubot Auth allows you to create and manage user sessions for access to data that is secured by access policies. */ auth: PasubotAuthClient // Commented by Tamil on 27-jun-2024 // realtime: RealtimeClient // protected realtimeUrl: string protected authUrl: string protected restUrl: string protected storageUrl: string protected functionsUrl: string // protected _rests: PostgrestClient protected rest: MongoDBClient protected storageKey: string protected fetch?: Fetch protected changedAccessToken?: string protected headers: Record /** * Create a new client for use in the browser. * @param pasubotUrl The unique Pasubot URL which is supplied when you create a new project in your project dashboard. * @param pasubotKey The unique Pasubot 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 Pasubot. * @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.global.fetch A custom fetch implementation. * @param options.global.headers Any additional headers to send with each network request. */ constructor( protected pasubotUrl: string, protected pasubotKey: string, options?: PasubotClientOptions ) { if (!pasubotUrl) throw new Error('pasubotUrl is required.') if (!pasubotKey) throw new Error('pasubotKey is required.') const _pasubotUrl = stripTrailingSlash(pasubotUrl) // Commented by Tamil on 27-jun-2024 // this.realtimeUrl = `${_pasubotUrl}/realtime/v1`.replace(/^http/i, 'ws') this.authUrl = `${_pasubotUrl}/auth/v1` this.restUrl = `${_pasubotUrl}/rest/v1` this.storageUrl = `${_pasubotUrl}/storage/v1` this.functionsUrl = `${_pasubotUrl}/functions/v1` // default storage key uses the pasubot project ref as a namespace const defaultStorageKey = `sb-${new URL(this.authUrl).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 ?? {} this.auth = this._initPasubotAuthClient( settings.auth ?? {}, this.headers, settings.global?.fetch ) this.fetch = fetchWithAuth(pasubotKey, this._getAccessToken.bind(this), settings.global?.fetch) // Commented by Tamil on 27-jun-2024 // this.realtime = this._initRealtimeClient({ headers: this.headers, ...settings.realtime }) // this.rest = new PostgrestClient(`${_pasubotUrl}/rest/v1`, { // headers: this.headers, // schema: settings.db?.schema, // fetch: this.fetch, // }) this.rest = new MongoDBClient(`${_pasubotUrl}/rest/v1`, { headers: this.headers, schema: settings.db?.schema, fetch: this.fetch, }) this._listenForAuthEvents() } /** * Pasubot Functions allows you to deploy and invoke edge functions. */ // Commented by Tamil on 27-jun-2024 // get functions() { // return new FunctionsClient(this.functionsUrl, { // headers: this.headers, // customFetch: this.fetch, // }) // } /** * Pasubot Storage allows you to manage user-generated content, such as photos or videos. */ get storage() { return new PasubotStorageClient(this.storageUrl, this.headers, this.fetch) } // NOTE: signatures must be kept in sync with MongoDBClient.from from< TableName extends string & keyof Schema['Tables'], Table extends Schema['Tables'][TableName] >(relation: TableName): MongoDBQueryBuilder from( relation: ViewName ): MongoDBQueryBuilder /** * Perform a query on a table or a view. * * @param relation - The table or view name to query */ from(relation: string): MongoDBQueryBuilder { return this.rest.from(relation) } // NOTE: signatures must be kept in sync with MongoDBClient.schema /** * Select a schema to query or perform an function (rpc) call. * * The schema needs to be on the list of exposed schemas inside Pasubot. * * @param schema - The schema to query */ schema( schema: DynamicSchema ): MongoDBClient< Database, DynamicSchema, Database[DynamicSchema] extends GenericSchema ? Database[DynamicSchema] : any > { return this.rest.schema(schema) } // NOTE: signatures must be kept in sync with MongoDBClient.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( fn: FnName, args: Fn['Args'] = {}, options: { head?: boolean get?: boolean count?: 'exact' | 'planned' | 'estimated' } = {} ): MongoDBFilterBuilder< Schema, Fn['Returns'] extends any[] ? Fn['Returns'][number] extends Record ? Fn['Returns'][number] : never : never, Fn['Returns'] > { return this.rest.rpc(fn, args, options) } /** * 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. * */ // Commented by Tamil on 27-jun-2024 // channel(name: string, opts: RealtimeChannelOptions = { config: {} }): RealtimeChannel { // return this.realtime.channel(name, opts) // } /** * Returns all Realtime channels. */ // Commented by Tamil on 27-jun-2024 // getChannels(): RealtimeChannel[] { // return this.realtime.getChannels() // } /** * Unsubscribes and removes Realtime channel from Realtime client. * * @param {RealtimeChannel} channel - The name of the Realtime channel. * */ // Commented by Tamil on 27-jun-2024 // removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> { // return this.realtime.removeChannel(channel) // } /** * Unsubscribes and removes all Realtime channels from Realtime client. */ // Commented by Tamil on 27-jun-2024 // removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> { // return this.realtime.removeAllChannels() // } private async _getAccessToken() { const { data } = await this.auth.getSession() return data.session?.access_token ?? null } private _initPasubotAuthClient( { autoRefreshToken, persistSession, detectSessionInUrl, storage, storageKey, flowType, debug, }: PasubotAuthClientOptions, headers?: Record, fetch?: Fetch ) { const authHeaders = { Authorization: `Bearer ${this.pasubotKey}`, apikey: `${this.pasubotKey}`, } return new PasubotAuthClient({ url: this.authUrl, headers: { ...authHeaders, ...headers }, storageKey: storageKey, autoRefreshToken, persistSession, detectSessionInUrl, storage, flowType, debug, 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: 'Authorization' in this.headers ?? false, }) } // Commented by Tamil on 27-jun-2024 // private _initRealtimeClient(options: RealtimeClientOptions) { // return new RealtimeClient(this.realtimeUrl, { // ...options, // params: { ...{ apikey: this.pasubotKey }, ...options?.params }, // }) // } private _listenForAuthEvents() { let 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 ) { // Token has changed // Commented by Tamil on 27-jun-2024 // this.realtime.setAuth(token ?? null) this.changedAccessToken = token } else if (event === 'SIGNED_OUT') { // Token is removed // Commented by Tamil on 27-jun-2024 // this.realtime.setAuth(this.pasubotKey) if (source == 'STORAGE') this.auth.signOut() this.changedAccessToken = undefined } } }