265 lines
8.0 KiB
TypeScript
265 lines
8.0 KiB
TypeScript
import { createClient, type SupabaseClient } from "@supabase/supabase-js";
|
|
import { getMockTableData } from "./mock-data";
|
|
|
|
const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
|
|
// Auto-enable mock mode when no Supabase URL is configured (demo/deployment without backend)
|
|
const useMockData = process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true" || !supabaseUrl || !supabaseUrl.includes("supabase.co");
|
|
|
|
// Mock query builder that supports all common Supabase methods
|
|
class MockQueryBuilder {
|
|
private data: unknown[];
|
|
private tableName: string;
|
|
private filters: { column: string; value: unknown; op: string }[] = [];
|
|
private selectColumns: string = "*";
|
|
private orderColumn?: string;
|
|
private orderDirection?: "asc" | "desc";
|
|
private limitValue?: number;
|
|
|
|
constructor(tableName: string) {
|
|
this.tableName = tableName;
|
|
this.data = [...(getMockTableData(tableName) || [])];
|
|
}
|
|
|
|
select(columns: string = "*") {
|
|
this.selectColumns = columns;
|
|
return this;
|
|
}
|
|
|
|
eq(column: string, value: unknown) {
|
|
this.filters.push({ column, value, op: "eq" });
|
|
return this;
|
|
}
|
|
|
|
neq(column: string, value: unknown) {
|
|
this.filters.push({ column, value, op: "neq" });
|
|
return this;
|
|
}
|
|
|
|
gt(column: string, value: unknown) {
|
|
this.filters.push({ column, value, op: "gt" });
|
|
return this;
|
|
}
|
|
|
|
gte(column: string, value: unknown) {
|
|
this.filters.push({ column, value, op: "gte" });
|
|
return this;
|
|
}
|
|
|
|
lt(column: string, value: unknown) {
|
|
this.filters.push({ column, value, op: "lt" });
|
|
return this;
|
|
}
|
|
|
|
lte(column: string, value: unknown) {
|
|
this.filters.push({ column, value, op: "lte" });
|
|
return this;
|
|
}
|
|
|
|
is(column: string, value: unknown) {
|
|
this.filters.push({ column, value, op: "is" });
|
|
return this;
|
|
}
|
|
|
|
like(column: string, value: string) {
|
|
this.filters.push({ column, value, op: "like" });
|
|
return this;
|
|
}
|
|
|
|
ilike(column: string, value: string) {
|
|
this.filters.push({ column, value, op: "ilike" });
|
|
return this;
|
|
}
|
|
|
|
in(column: string, values: unknown[]) {
|
|
this.filters.push({ column, value: values, op: "in" });
|
|
return this;
|
|
}
|
|
|
|
order(column: string, options?: { ascending?: boolean; nullsFirst?: boolean }) {
|
|
this.orderColumn = column;
|
|
this.orderDirection = options?.ascending === false ? "desc" : "asc";
|
|
return this;
|
|
}
|
|
|
|
limit(count: number) {
|
|
this.limitValue = count;
|
|
return this;
|
|
}
|
|
|
|
range(min: number, max: number) {
|
|
// For pagination mock
|
|
return this;
|
|
}
|
|
|
|
single() {
|
|
return this.executeSingle();
|
|
}
|
|
|
|
then(resolve: (value: unknown) => void, _reject?: (reason?: unknown) => void) {
|
|
const result = this.execute();
|
|
resolve(result);
|
|
}
|
|
|
|
async executeSingle() {
|
|
const result = this.execute();
|
|
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
|
|
return { data: result.data[0], error: null };
|
|
}
|
|
return { data: null, error: null };
|
|
}
|
|
|
|
execute() {
|
|
let filtered = [...this.data];
|
|
|
|
// Apply filters
|
|
for (const filter of this.filters) {
|
|
filtered = filtered.filter((row: any) => {
|
|
const rowValue = row[filter.column];
|
|
switch (filter.op) {
|
|
case "eq":
|
|
return rowValue === filter.value;
|
|
case "neq":
|
|
return rowValue !== filter.value;
|
|
case "gt":
|
|
return (rowValue as number) > (filter.value as number);
|
|
case "gte":
|
|
return (rowValue as number) >= (filter.value as number);
|
|
case "lt":
|
|
return (rowValue as number) < (filter.value as number);
|
|
case "lte":
|
|
return (rowValue as number) <= (filter.value as number);
|
|
case "is":
|
|
if (filter.value === null) return rowValue === null;
|
|
if (filter.value === undefined) return rowValue === undefined;
|
|
return rowValue === filter.value;
|
|
case "like":
|
|
return typeof rowValue === "string" && rowValue.includes((filter.value as string).replace(/%/g, ""));
|
|
case "ilike":
|
|
return typeof rowValue === "string" && rowValue.toLowerCase().includes((filter.value as string).replace(/%/g, "").toLowerCase());
|
|
case "in":
|
|
return Array.isArray(filter.value) && filter.value.includes(rowValue);
|
|
default:
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Apply ordering
|
|
if (this.orderColumn) {
|
|
filtered.sort((a: any, b: any) => {
|
|
const aVal = a[this.orderColumn!];
|
|
const bVal = b[this.orderColumn!];
|
|
if (aVal < bVal) return this.orderDirection === "desc" ? 1 : -1;
|
|
if (aVal > bVal) return this.orderDirection === "desc" ? -1 : 1;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
// Apply limit
|
|
if (this.limitValue !== undefined) {
|
|
filtered = filtered.slice(0, this.limitValue);
|
|
}
|
|
|
|
return { data: filtered, error: null };
|
|
}
|
|
}
|
|
|
|
// Mock insert/update/delete builders
|
|
class MockMutationBuilder {
|
|
private tableName: string;
|
|
private data: Record<string, unknown> | Record<string, unknown>[];
|
|
|
|
constructor(tableName: string, data: Record<string, unknown> | Record<string, unknown>[]) {
|
|
this.tableName = tableName;
|
|
this.data = data;
|
|
}
|
|
|
|
select() {
|
|
return new MockQueryBuilder(this.tableName);
|
|
}
|
|
|
|
then(resolve: (value: unknown) => void) {
|
|
const items = Array.isArray(this.data) ? this.data : [this.data];
|
|
const returning = items.map((item, i) => ({
|
|
...item,
|
|
id: item.id || `generated-${Date.now()}-${i}`,
|
|
}));
|
|
resolve({ data: returning, error: null });
|
|
}
|
|
}
|
|
|
|
// Mock storage builder
|
|
class MockStorageBuilder {
|
|
from(bucket: string) {
|
|
return {
|
|
upload: async (path: string, _file: unknown) => {
|
|
return { data: { path }, error: null };
|
|
},
|
|
download: async (path: string) => {
|
|
return { data: new Blob(), error: null };
|
|
},
|
|
remove: async (paths: string[]) => {
|
|
return { data: { paths }, error: null };
|
|
},
|
|
list: async () => {
|
|
return { data: [], error: null };
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// Create mock client
|
|
function createMockClient() {
|
|
return {
|
|
from: (table: string) => new MockQueryBuilder(table),
|
|
insert: (data: Record<string, unknown> | Record<string, unknown>[]) => new MockMutationBuilder("unknown", data),
|
|
update: (data: Record<string, unknown>) => new MockMutationBuilder("unknown", data),
|
|
delete: () => ({
|
|
eq: () => ({ then: (resolve: (value: unknown) => void) => resolve({ data: null, error: null }) }),
|
|
in: () => ({ then: (resolve: (value: unknown) => void) => resolve({ data: null, error: null }) }),
|
|
}),
|
|
storage: new MockStorageBuilder(),
|
|
auth: {
|
|
getSession: async () => ({ data: { session: null }, error: null }),
|
|
getUser: async () => ({ data: { user: null }, error: null }),
|
|
signInWithPassword: async () => ({ data: { user: null, session: null }, error: null }),
|
|
signOut: async () => ({ error: null }),
|
|
onAuthStateChange: () => ({ data: { subscription: { unsubscribe: () => {} } } }),
|
|
},
|
|
channel: () => ({
|
|
on: () => ({ subscribe: () => ({}) }),
|
|
subscribe: () => ({}),
|
|
}),
|
|
};
|
|
}
|
|
|
|
// Real Supabase client creation
|
|
function getSupabase(): SupabaseClient {
|
|
if (!supabaseUrl || !supabaseAnonKey) {
|
|
throw new Error(
|
|
`Missing Supabase env vars: ${[!supabaseUrl && "NEXT_PUBLIC_SUPABASE_URL", !supabaseAnonKey && "NEXT_PUBLIC_SUPABASE_ANON_KEY"].filter(Boolean).join(", ")}. ` +
|
|
"Check Vercel environment variables for Production environment. " +
|
|
"Node env: " + (process.env.NODE_ENV ?? "unknown")
|
|
);
|
|
}
|
|
return createClient(supabaseUrl, supabaseAnonKey);
|
|
}
|
|
|
|
// Create proxy that routes to real or mock client
|
|
let realSupabase: SupabaseClient | null = null;
|
|
if (!useMockData) {
|
|
try {
|
|
realSupabase = getSupabase();
|
|
} catch {
|
|
// Will use mock below
|
|
}
|
|
}
|
|
|
|
export const supabase: SupabaseClient = useMockData || !realSupabase
|
|
? createMockClient() as unknown as SupabaseClient
|
|
: realSupabase;
|
|
|
|
export { supabaseUrl, supabaseAnonKey, useMockData };
|