Files
Compass/common/src/supabase/utils.ts
2025-10-09 17:59:10 +02:00

177 lines
5.0 KiB
TypeScript

import {
createClient as createClientGeneric,
PostgrestResponse,
PostgrestSingleResponse,
SupabaseClient as SupabaseClientGeneric,
SupabaseClientOptions as SupabaseClientOptionsGeneric,
} from '@supabase/supabase-js'
import {Database} from './schema'
import {User} from '../user'
export type Schema = Database['public']
export type Tables = Schema['Tables']
export type Views = Schema['Views']
export type TableName = keyof Tables
export type ViewName = keyof Views
export type Selectable = TableName | ViewName
export type Row<T extends Selectable> = T extends TableName
? Tables[T]['Row']
: T extends ViewName
? Views[T]['Row']
: never
export type Column<T extends Selectable> = keyof Row<T> & string
export type SupabaseClient = SupabaseClientGeneric<Database, 'public', Schema>
export function createClient(
instanceId: string,
key: string,
opts?: SupabaseClientOptionsGeneric<'public'>
) {
const url = `https://${instanceId}.supabase.co`
// console.debug('createClient', instanceId, key, opts)
return createClientGeneric(
url,
key,
opts
// {
// auth: {
// persistSession: false, // ✅ No localStorage
// autoRefreshToken: false, // ✅ No token refresh
// },
// global: {
// headers: {
// Authorization: `Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imx0emVweG5oaG5ybnZvdnFibGZyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NTU5NjczNjgsImV4cCI6MjA3MTU0MzM2OH0.pbazcrVOG7Kh_IgblRu2VAfoBe3-xheNfRzAto7xvzY`, // ✅ Force anon
// },
// },
// }
) as SupabaseClient
}
export type QueryResponse<T> = PostgrestResponse<T> | PostgrestSingleResponse<T>
export type QueryMultiSuccessResponse<T> = { data: T[]; count: number }
export type QuerySingleSuccessResponse<T> = { data: T; count: number }
export async function run<T>(
q: PromiseLike<PostgrestResponse<T>>
): Promise<QueryMultiSuccessResponse<T>>
export async function run<T>(
q: PromiseLike<PostgrestSingleResponse<T>>
): Promise<QuerySingleSuccessResponse<T>>
export async function run<T>(
q: PromiseLike<PostgrestSingleResponse<T> | PostgrestResponse<T>>
) {
const {data, count, error} = await q
if (error != null) {
throw error
} else {
return {data, count}
}
}
type JsonTypes = {
users: User
}
export type DataFor<T extends Selectable> = T extends keyof JsonTypes
? JsonTypes[T]
: any
export function selectJson<T extends TableName | ViewName>(
db: SupabaseClient,
table: T
) {
return db.from(table).select<string, { data: DataFor<T> }>('data')
}
export function selectFrom<
T extends TableName,
TData extends DataFor<T>,
TFields extends (string & keyof TData)[],
TResult = Pick<TData, TFields[number]>
>(db: SupabaseClient, table: T, ...fields: TFields) {
const query = fields.map((f) => `data->${f}`).join(', ')
return db.from(table).select<string, TResult>(query)
}
export function millisToTs(millis: number) {
return new Date(millis).toISOString()
}
export function tsToMillis(ts: string) {
return Date.parse(ts)
}
type SnakeToCamel<S extends string> = S extends `${infer T}_${infer U}`
? `${Lowercase<T>}${Capitalize<SnakeToCamel<U>>}`
: S
const camelize = <S extends string>(s: S) =>
s.replace(/(_\w)/g, (m) => m[1].toUpperCase()) as SnakeToCamel<S>
// sql column -> converter function or false
type TypeConverter<R extends Selectable, T extends Record<string, any>> = {
[key in Column<R>]?: SnakeToCamel<key> extends keyof T
? ((r: Row<R>[key]) => T[SnakeToCamel<key>]) | false
: false
}
/**
* Convert a sql row to its frontend data type.
* Changes snake_case to camelCase.
* You can also specify conversion functions for each column, or set it to false to filter it.
*/
export const convertSQLtoTS = <
R extends Selectable,
T extends Record<string, any>
>(
sqlData: Partial<Row<R> & { data: any }>,
converters: TypeConverter<R, T>,
expandData = true,
shouldCamelize = true
) => {
const {data = {}, ...rows} = sqlData
const entries = Object.entries(rows)
const m = entries
.map((entry) => {
const [key, val] = entry as [Column<R>, Row<R>[Column<R>]]
const convert = converters[key]
if (convert === false) return null
const jsProp = shouldCamelize ? camelize(key) : key
const jsVal = convert != null ? convert(val) : val
return [jsProp, jsVal]
})
.filter((x) => x != null)
const newRows = Object.fromEntries(m as any)
if (expandData) return {...data, ...newRows} as T
else return {...newRows} as T
}
export const convertObjectToSQLRow = <
T extends Record<string, any>,
R extends Selectable
>(
objData: Partial<T>
) => {
const entries = Object.entries(objData)
const m = entries
.map((entry) => {
const [key, val] = entry as [string, T[keyof T]]
const decamelizeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase()
return [decamelizeKey, val]
})
.filter((x) => x != null)
const newRows = Object.fromEntries(m as any)
return newRows as Partial<Row<R> & { data: any }>
}