Action

Type

Resolved On

Supabase Query Wrapper refactoring - - -

Supabase Query Wrapper

Overview

Most Supabase queries follow an identical pattern with repetitive error handling. A wrapper function could reduce boilerplate and standardize error handling.

Problem

The same query pattern repeats throughout the lib files:

// Pattern seen 20+ times across the codebase
const result = await supabase
  .from("Table")
  .select("*")
  .eq("field", value)
  // ... more conditions

if (result.error) {
  console.error("Error in Fetching X", result.error.message);
  // GA - Error Handling (placeholder comment, never implemented)
  return [];
} else {
  // Process result.data
  return processedData;
}

Issues

  1. Repetition: Same error handling pattern copied everywhere
  2. Placeholder Comments: “GA - Error Handling” comments without implementation
  3. Silent Failures: Returns empty arrays instead of propagating errors
  4. Inconsistent Logging: Error messages vary in format

Solution

Create a query wrapper utility in src/lib/db.ts with standardized error handling.

Option 1: Simple Wrapper

// src/lib/db.ts
import { supabase } from "./supabase";
import type { PostgrestBuilder } from "@supabase/postgrest-js";

export async function executeQuery<T>(
  query: PostgrestBuilder<T>,
  operationName: string,
  defaultValue: T
): Promise<T> {
  const result = await query;

  if (result.error) {
    console.error(`Error in ${operationName}:`, result.error.message);
    // TODO: Send to error tracking service (Sentry, etc.)
    return defaultValue;
  }

  return result.data as T;
}
// src/lib/db.ts
export interface QueryResult<T> {
  data: T;
  error: string | null;
  success: boolean;
}

export async function query<T>(
  operation: () => Promise<{ data: T | null; error: { message: string } | null }>,
  operationName: string
): Promise<QueryResult<T>> {
  try {
    const { data, error } = await operation();

    if (error) {
      console.error(`Error in ${operationName}:`, error.message);
      return { data: null as T, error: error.message, success: false };
    }

    return { data: data as T, error: null, success: true };
  } catch (err) {
    const message = err instanceof Error ? err.message : "Unknown error";
    console.error(`Exception in ${operationName}:`, message);
    return { data: null as T, error: message, success: false };
  }
}

// Usage
const { data, error, success } = await query(
  () => supabase.from("Ideas").select("*").eq("project", project),
  "Fetching Ideas"
);

if (!success) {
  // Handle error - maybe show user notification
  return [];
}

Option 3: Class-Based Builder

// src/lib/db.ts
export class QueryBuilder<T> {
  private query: any;
  private operationName: string;

  constructor(query: any, operationName: string) {
    this.query = query;
    this.operationName = operationName;
  }

  async execute(): Promise<T | null> {
    const result = await this.query;

    if (result.error) {
      console.error(`Error in ${this.operationName}:`, result.error.message);
      return null;
    }

    return result.data;
  }

  async executeOrDefault(defaultValue: T): Promise<T> {
    return (await this.execute()) ?? defaultValue;
  }
}

// Usage
const ideas = await new QueryBuilder(
  supabase.from("Ideas").select("*"),
  "Fetching Ideas"
).executeOrDefault([]);

Benefits

  • Reduced Boilerplate: Eliminates repetitive error handling
  • Consistent Logging: All errors logged with consistent format
  • Error Propagation: Can choose to handle or propagate errors
  • Testability: Query wrapper can be mocked for testing
  • Future-Proof: Easy to add metrics, caching, or retry logic

Implementation Notes

  • Start with Option 1 or 2 for simplicity
  • Consider integrating with an error tracking service
  • Keep the original supabase client accessible for complex queries
  • Document when to use wrapper vs. raw supabase client
  • src/lib/supabase.ts
  • src/lib/monthly.ts
  • src/lib/yearly.ts
  • src/lib/weekly.ts
  • All other lib files with Supabase queries