UNPKG

@dataql/supabase-adapter

Version:

Supabase adapter for DataQL with zero API changes

619 lines (558 loc) 15.3 kB
import { Data, DataOptions, ID, String, Int, Boolean, Decimal, Date as DataQLDate, } from "@dataql/core"; // Supabase-like configuration export interface SupabaseClientOptions { // Required for DataQL infrastructure routing appToken?: string; // Database name for client isolation dbName?: string; // Environment routing env?: "dev" | "prod"; // Development prefix devPrefix?: string; // Optional custom connection customConnection?: any; // Supabase-specific options (for compatibility) auth?: { autoRefreshToken?: boolean; persistSession?: boolean; detectSessionInUrl?: boolean; }; realtime?: { params?: Record<string, any>; }; global?: { headers?: Record<string, string>; }; } // Supabase response types export interface SupabaseResponse<T> { data: T | null; error: SupabaseError | null; count?: number | null; status: number; statusText: string; } export interface SupabaseError { message: string; details?: string; hint?: string; code?: string; } // Filter operators export type FilterOperator = | "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "like" | "ilike" | "match" | "imatch" | "in" | "cs" | "cd" | "sl" | "sr" | "nxl" | "nxr" | "adj" | "ov" | "fts" | "plfts" | "phfts" | "wfts"; // Real-time events export type RealtimeSubscriptionEvent = "INSERT" | "UPDATE" | "DELETE" | "*"; export interface RealtimePayload<T = any> { commit_timestamp: string; eventType: RealtimeSubscriptionEvent; schema: string; table: string; new?: T; old?: T; errors?: string[]; } export interface RealtimeSubscription { unsubscribe: () => void; } // Query builder class export class SupabaseQueryBuilder<T = any> { private _data: Data; private _tableName: string; private _collection: any; private _selectFields?: string[]; private _filters: Array<{ column: string; operator: FilterOperator; value: any; }> = []; private _orderBy?: { column: string; ascending: boolean }; private _limit?: number; private _offset?: number; private _single = false; private _count?: "exact" | "planned" | "estimated"; constructor(data: Data, tableName: string, schema?: any) { this._data = data; this._tableName = tableName; this._collection = data.collection( tableName, schema || this._getDefaultSchema() ); } private _getDefaultSchema() { // Default schema for tables without explicit schema return { id: { type: "ID", required: true }, created_at: { type: "Date", default: "now" }, updated_at: { type: "Date", default: "now" }, }; } // Select columns select( columns?: string, options?: { count?: "exact" | "planned" | "estimated" } ): this { if (columns) { this._selectFields = columns.split(",").map((col) => col.trim()); } if (options?.count) { this._count = options.count; } return this; } // Filtering methods eq(column: string, value: any): this { this._filters.push({ column, operator: "eq", value }); return this; } neq(column: string, value: any): this { this._filters.push({ column, operator: "neq", value }); return this; } gt(column: string, value: any): this { this._filters.push({ column, operator: "gt", value }); return this; } gte(column: string, value: any): this { this._filters.push({ column, operator: "gte", value }); return this; } lt(column: string, value: any): this { this._filters.push({ column, operator: "lt", value }); return this; } lte(column: string, value: any): this { this._filters.push({ column, operator: "lte", value }); return this; } like(column: string, pattern: string): this { this._filters.push({ column, operator: "like", value: pattern }); return this; } ilike(column: string, pattern: string): this { this._filters.push({ column, operator: "ilike", value: pattern }); return this; } in(column: string, values: any[]): this { this._filters.push({ column, operator: "in", value: values }); return this; } contains(column: string, value: any): this { this._filters.push({ column, operator: "cs", value }); return this; } containedBy(column: string, value: any): this { this._filters.push({ column, operator: "cd", value }); return this; } textSearch(column: string, query: string): this { this._filters.push({ column, operator: "fts", value: query }); return this; } // Range filtering range(from: number, to: number): this { this._offset = from; this._limit = to - from + 1; return this; } // Ordering order( column: string, options?: { ascending?: boolean; nullsFirst?: boolean } ): this { this._orderBy = { column, ascending: options?.ascending !== false }; return this; } // Limit limit(count: number): this { this._limit = count; return this; } // Single result single(): this { this._single = true; this._limit = 1; return this; } // Maybe single result maybeSingle(): this { this._limit = 1; return this; } // Execute query and return response async execute(): Promise<SupabaseResponse<T | T[]>> { try { const filter = this._buildDataQLFilter(); let results = await this._collection.find(filter); // Apply ordering if (this._orderBy) { results = this._applyOrderBy(results); } // Apply pagination if (this._offset) { results = results.slice(this._offset); } if (this._limit) { results = results.slice(0, this._limit); } // Apply field selection if (this._selectFields) { results = results.map((item: any) => this._applyFieldSelection(item)); } // Return single or array based on query type const data = this._single ? results[0] || null : results; const count = this._count ? results.length : null; return { data: data as T | T[], error: null, count, status: 200, statusText: "OK", }; } catch (error) { return { data: null, error: { message: error instanceof Error ? error.message : "Unknown error", code: "DATAQL_ERROR", }, status: 500, statusText: "Internal Server Error", }; } } private _buildDataQLFilter(): any { const filter: any = {}; for (const filterItem of this._filters) { const { column, operator, value } = filterItem; switch (operator) { case "eq": filter[column] = value; break; case "neq": filter[column] = { $ne: value }; break; case "gt": filter[column] = { $gt: value }; break; case "gte": filter[column] = { $gte: value }; break; case "lt": filter[column] = { $lt: value }; break; case "lte": filter[column] = { $lte: value }; break; case "like": case "ilike": filter[column] = { $regex: value, $options: operator === "ilike" ? "i" : "", }; break; case "in": filter[column] = { $in: value }; break; case "cs": filter[column] = { $in: Array.isArray(value) ? value : [value] }; break; case "fts": filter[column] = { $text: { $search: value } }; break; default: filter[column] = value; } } return filter; } private _applyOrderBy(results: any[]): any[] { if (!this._orderBy) return results; const { column, ascending } = this._orderBy; return results.sort((a, b) => { const aVal = a[column]; const bVal = b[column]; if (aVal < bVal) return ascending ? -1 : 1; if (aVal > bVal) return ascending ? 1 : -1; return 0; }); } private _applyFieldSelection(item: any): any { if (!this._selectFields) return item; const result: any = {}; for (const field of this._selectFields) { if (field.includes(".")) { // Handle nested field selection (simplified) const parts = field.split("."); let value = item; for (const part of parts) { value = value?.[part]; } result[field] = value; } else { result[field] = item[field]; } } return result; } // Insert data async insert( data: Partial<T> | Partial<T>[] ): Promise<SupabaseResponse<T | T[]>> { try { const result = await this._collection.create(data); return { data: result, error: null, status: 201, statusText: "Created", }; } catch (error) { return { data: null, error: { message: error instanceof Error ? error.message : "Insert failed", code: "INSERT_ERROR", }, status: 400, statusText: "Bad Request", }; } } // Update data async update(data: Partial<T>): Promise<SupabaseResponse<T | T[]>> { try { const filter = this._buildDataQLFilter(); const result = await this._collection.update(filter, data); return { data: result, error: null, status: 200, statusText: "OK", }; } catch (error) { return { data: null, error: { message: error instanceof Error ? error.message : "Update failed", code: "UPDATE_ERROR", }, status: 400, statusText: "Bad Request", }; } } // Upsert data async upsert( data: Partial<T> | Partial<T>[], options?: { onConflict?: string } ): Promise<SupabaseResponse<T | T[]>> { try { const result = await this._collection.upsert(data); return { data: result, error: null, status: 200, statusText: "OK", }; } catch (error) { return { data: null, error: { message: error instanceof Error ? error.message : "Upsert failed", code: "UPSERT_ERROR", }, status: 400, statusText: "Bad Request", }; } } // Delete data async delete(): Promise<SupabaseResponse<T | T[]>> { try { const filter = this._buildDataQLFilter(); const result = await this._collection.delete(filter); return { data: result, error: null, status: 200, statusText: "OK", }; } catch (error) { return { data: null, error: { message: error instanceof Error ? error.message : "Delete failed", code: "DELETE_ERROR", }, status: 400, statusText: "Bad Request", }; } } // Real-time subscription on( event: RealtimeSubscriptionEvent, callback: (payload: RealtimePayload<T>) => void ): RealtimeSubscription { // Mock real-time subscription for now // In a real implementation, this would use DataQL's real-time capabilities console.log(`Subscribed to ${event} events on table ${this._tableName}`); return { unsubscribe: () => { console.log( `Unsubscribed from ${event} events on table ${this._tableName}` ); }, }; } } // Supabase client class export class SupabaseClient { private _data: Data; private _schemas: Map<string, any> = new Map(); constructor( supabaseUrl: string, supabaseKey: string, options?: SupabaseClientOptions ) { // Ensure proper DataQL infrastructure routing (Client → Worker → Lambda → Database) const dataqlOptions = { appToken: options?.appToken || "default", // Required for DataQL routing env: options?.env || "prod", // Routes to production infrastructure devPrefix: options?.devPrefix || "dev_", // Optional prefix for dev environments dbName: options?.dbName, // Database isolation per client customConnection: options?.customConnection, // Optional custom connection }; this._data = new Data(dataqlOptions); } // Register table schema registerTable(tableName: string, schema: any): void { this._schemas.set(tableName, schema); } // Get table query builder from<T = any>(table: string): SupabaseQueryBuilder<T> { const schema = this._schemas.get(table); return new SupabaseQueryBuilder<T>(this._data, table, schema); } // Authentication methods (mocked for now) get auth() { return { signUp: async (credentials: any) => ({ data: null, error: null }), signIn: async (credentials: any) => ({ data: null, error: null }), signOut: async () => ({ error: null }), getUser: async () => ({ data: null, error: null }), getSession: async () => ({ data: null, error: null }), onAuthStateChange: (callback: Function) => ({ data: { subscription: { unsubscribe: () => {} } }, }), }; } // Storage methods (mocked for now) get storage() { return { from: (bucket: string) => ({ upload: async (path: string, file: any) => ({ data: null, error: null, }), download: async (path: string) => ({ data: null, error: null }), remove: async (paths: string[]) => ({ data: null, error: null }), list: async (path?: string) => ({ data: [], error: null }), getPublicUrl: (path: string) => ({ data: { publicUrl: "" } }), }), }; } // RPC (Remote Procedure Call) methods async rpc<T = any>( fn: string, args?: Record<string, any>, options?: { count?: "exact" | "planned" | "estimated" } ): Promise<SupabaseResponse<T>> { // Mock RPC calls for now return { data: null, error: { message: "RPC not yet supported in DataQL adapter", code: "RPC_NOT_SUPPORTED", }, status: 501, statusText: "Not Implemented", }; } // Channel for real-time channel(name: string, options?: { config?: Record<string, any> }) { return { on: ( type: "postgres_changes", filter: { event: RealtimeSubscriptionEvent; schema: string; table: string; }, callback: (payload: RealtimePayload) => void ) => { // Mock channel subscription console.log( `Subscribed to postgres_changes on ${filter.schema}.${filter.table}` ); return { unsubscribe: () => { console.log( `Unsubscribed from postgres_changes on ${filter.schema}.${filter.table}` ); }, }; }, subscribe: () => { console.log(`Subscribed to channel ${name}`); return Promise.resolve(); }, unsubscribe: () => { console.log(`Unsubscribed from channel ${name}`); return Promise.resolve(); }, }; } } // Create Supabase client export function createClient( supabaseUrl: string, supabaseKey: string, options?: SupabaseClientOptions ): SupabaseClient { return new SupabaseClient(supabaseUrl, supabaseKey, options); } // Export main client export default createClient;