quickli
Version:
Easy-to-use Supabase client with automatic configuration and connection management
422 lines (360 loc) • 10.5 kB
JavaScript
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;