UNPKG

quickli

Version:

Easy-to-use Supabase client with automatic configuration and connection management

422 lines (360 loc) 10.5 kB
const { createClient } = require('@supabase/supabase-js'); const pRetry = require('p-retry'); const pTimeout = require('p-timeout'); require('dotenv').config(); class SupabaseClient { constructor(url, key, options = {}) { this.url = url || process.env.SUPABASE_URL; this.key = key || process.env.SUPABASE_KEY; if (!this.url || !this.key) { throw new Error( 'Supabase credentials not found. Please provide them either:\n' + '1. As constructor parameters: new SupabaseClient(url, key)\n' + '2. As environment variables: SUPABASE_URL and SUPABASE_KEY' ); } this.options = { retries: 3, timeout: 30000, pageSize: 100, ...options }; this.client = createClient(this.url, this.key, { auth: { autoRefreshToken: true, persistSession: true, detectSessionInUrl: false }, db: { schema: 'public' }, global: { headers: { 'x-client-info': 'supabase-easy-client' } } }); } async _withRetry(operation) { return pTimeout( pRetry( async (attempt) => { try { return await operation(); } catch (error) { if (error.status === 429) { // Add exponential backoff delay before retry const delay = Math.min(1000 * Math.pow(2, attempt), 10000); await new Promise(resolve => setTimeout(resolve, delay)); // Retry the operation after delay return operation(); } throw error; } }, { retries: this.options.retries } ), this.options.timeout ); } from(table) { const query = this.client.from(table); const builder = { select: (...args) => { query.select(...args); return builder; }, insert: (...args) => { query.insert(...args); return builder; }, update: (...args) => { query.update(...args); return builder; }, delete: (...args) => { query.delete(...args); return builder; }, upsert: (...args) => { query.upsert(...args); return builder; }, match: (filters) => { query.match(filters); return builder; }, eq: (column, value) => { query.eq(column, value); return builder; }, neq: (column, value) => { query.neq(column, value); return builder; }, gt: (column, value) => { query.gt(column, value); return builder; }, gte: (column, value) => { query.gte(column, value); return builder; }, lt: (column, value) => { query.lt(column, value); return builder; }, lte: (column, value) => { query.lte(column, value); return builder; }, like: (column, pattern) => { query.like(column, pattern); return builder; }, ilike: (column, pattern) => { query.ilike(column, pattern); return builder; }, is: (column, value) => { query.is(column, value); return builder; }, in: (column, values) => { query.in(column, values); return builder; }, contains: (column, value) => { query.contains(column, value); return builder; }, containedBy: (column, value) => { query.containedBy(column, value); return builder; }, range: (from, to) => { query.range(from, to); return builder; }, single: () => { query.single(); return builder; }, limit: (count) => { query.limit(count); return builder; }, order: (column, options) => { query.order(column, options); return builder; }, then: async (...args) => { return this._withRetry(() => query.then(...args)); } }; return builder; } auth() { return this.client.auth; } storage() { return this.client.storage; } rpc(fn, params) { return this._withRetry(() => this.client.rpc(fn, params)); } async testConnection() { try { await this._withRetry(async () => { const { data: { session }, error: authError } = await this.client.auth.getSession(); const { data, error } = await this.client .from('_test_connection') .select('count') .limit(1); if (error && !error.message?.includes('does not exist')) { throw error; } }); return true; } catch (error) { if (error.message?.includes('does not exist')) { return true; } throw error; } } async hasRealtimeSupport() { try { const channel = this.client.channel('test'); await this._withRetry(() => channel.subscribe()); channel.unsubscribe(); return true; } catch { return false; } } // Enhanced helper methods async findOne(table, query = {}) { const queryBuilder = this.from(table).select('*'); // Apply filters from query object Object.entries(query).forEach(([key, value]) => { queryBuilder.eq(key, value); }); const { data, error } = await queryBuilder .limit(1) .single(); if (error) throw error; return data; } async findMany(table, query = {}, options = {}) { const queryBuilder = this.from(table).select('*'); // Apply filters from query object Object.entries(query).forEach(([key, value]) => { queryBuilder.eq(key, value); }); // Apply ordering if (options.orderBy) { queryBuilder.order(options.orderBy, { ascending: options.ascending !== false }); } // Apply pagination const { data, error } = await queryBuilder .range(options.offset || 0, (options.offset || 0) + (options.limit || this.options.pageSize) - 1); if (error) throw error; return data; } async createOne(table, data) { const { data: result, error } = await this._withRetry(() => this.from(table) .insert(data) .select() .single() ); if (error) throw error; return result; } async createMany(table, records) { const { data: result, error } = await this.from(table) .insert(records) .select(); if (error) throw error; return result; } async updateOne(table, query, data) { const queryBuilder = this.from(table); // Apply filters from query object Object.entries(query).forEach(([key, value]) => { queryBuilder.eq(key, value); }); const { data: result, error } = await this._withRetry(() => queryBuilder .update(data) .select() .single() ); if (error) throw error; return result; } async updateMany(table, query, data) { const { data: result, error } = await this.from(table) .update(data) .match(query) .select(); if (error) throw error; return result; } async deleteOne(table, query) { const queryBuilder = this.from(table); // Apply filters from query object Object.entries(query).forEach(([key, value]) => { queryBuilder.eq(key, value); }); const { data: result, error } = await this._withRetry(() => queryBuilder .delete() .select() .single() ); if (error) throw error; return result; } async deleteMany(table, query) { const { data: result, error } = await this.from(table) .delete() .match(query) .select(); if (error) throw error; return result; } // Pagination helper async paginate(table, query = {}, options = {}) { const pageSize = options.pageSize || this.options.pageSize; const page = options.page || 1; const offset = (page - 1) * pageSize; const countBuilder = this.from(table).select('count', { count: 'exact' }); const queryBuilder = this.from(table).select('*'); // Apply filters from query object to both builders Object.entries(query).forEach(([key, value]) => { countBuilder.eq(key, value); queryBuilder.eq(key, value); }); // Get total count const { count, error: countError } = await this._withRetry(() => countBuilder); if (countError) throw countError; // Apply ordering and pagination if (options.orderBy) { queryBuilder.order(options.orderBy, { ascending: options.ascending !== false }); } const { data, error } = await this._withRetry(() => queryBuilder.range(offset, offset + pageSize - 1) ); if (error) throw error; return { data, pagination: { total: count, pageSize, page, totalPages: Math.ceil(count / pageSize) } }; } // Aggregation helpers async count(table, query = {}) { const queryBuilder = this.from(table).select('*', { count: 'exact' }); // Apply filters from query object Object.entries(query).forEach(([key, value]) => { queryBuilder.eq(key, value); }); const { count, error } = await this._withRetry(() => queryBuilder); if (error) throw error; return count; } async sum(table, column, query = {}) { const queryBuilder = this.from(table).select(`sum:${column}`); // Apply filters from query object Object.entries(query).forEach(([key, value]) => { queryBuilder.eq(key, value); }); const { data, error } = await this._withRetry(() => queryBuilder); if (error) throw error; return data[0]?.sum || 0; } async average(table, column, query = {}) { const queryBuilder = this.from(table).select(`avg:${column}`); // Apply filters from query object Object.entries(query).forEach(([key, value]) => { queryBuilder.eq(key, value); }); const { data, error } = await this._withRetry(() => queryBuilder); if (error) throw error; return data[0]?.avg || 0; } // Advanced filtering async search(table, column, query, options = {}) { const { data, error } = await this.from(table) .select('*') .textSearch(column, query, options); if (error) throw error; return data; } } // Always export the SupabaseClient class for consistent usage module.exports = SupabaseClient;