/** * Compatibility shim that preserves the historical Supabase client * query-builder API surface (`.from().select()...`) for the legacy * public-storefront and admin pages that still call into it. The SaaS * rebuild no longer uses Supabase as a backend — server actions and * API routes connect directly to Postgres via `pg` / `withDb` from * `@/db/client`. This shim exists so the build keeps passing while * the remaining legacy call sites are migrated to Drizzle / raw `pg` * queries. * * IMPORTANT: This shim does NOT talk to a real database. It returns * empty result sets. Legacy call sites that need real data must be * rewritten against `pool` / `withDb` / `withBrand`. * * The query-builder API surface supported here is intentionally narrow: * - .from(table).select(cols?).eq(col, val).eq(...).is(col, null). * order(col, opts?).limit(n).range(min, max).single() → returns * `{ data, error }` like the Supabase client did. * - .from(table).insert(payload).select().single() for legacy inserts * - .from(table).upsert(payload) * - .from(table).update(payload).eq(col, val) * - .from(table).delete().eq(col, val) * - .rpc(fnName, params) — returns `{ data, error }` (data is null) * - .auth.{getSession, getUser, signInWithPassword, signOut, * updateUser, onAuthStateChange} — all return null / no-ops * - .storage.from(bucket).{upload, download, remove, list} * - .channel().on().subscribe() * * If a call site needs more than that, migrate the call site. */ type FilterOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "is" | "like" | "ilike" | "in"; type Filter = { column: string; value: unknown; op: FilterOp }; // Result types for the mock query builder interface MockQueryResult { data: T[] | null; error: null; } interface MockSingleResult { data: T | null; error: null; } class MockQueryBuilder = Record> { private tableName: string; private filters: Filter[] = []; private selectColumns: string = "*"; private orderColumn?: string; private orderDirection?: "asc" | "desc"; private limitValue?: number; private rangeMin?: number; private rangeMax?: number; private mode: "select" | "update" | "delete" = "select"; private mutationData: Record | null = null; constructor(tableName: string) { this.tableName = tableName; // No mock data - return empty results } select(columns: string = "*", _opts?: Record) { 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) { this.rangeMin = min; this.rangeMax = max; return this; } // The legacy Supabase client returns a thenable from .single() so // callers can write `.single().then(({ data, error }) => ...)` as // well as `const { data, error } = await ....single()`. We return a // proper Promise> so destructured binding // patterns in callers work under `--strict` (no implicit `any`). single(): Promise> { return Promise.resolve(this.executeSingle()); } maybeSingle(): Promise> { return Promise.resolve(this.executeSingle()); } then, TResult2 = never>( onfulfilled?: ((value: MockQueryResult) => TResult1 | PromiseLike) | null, _onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null, ): PromiseLike { const result = this.execute(); return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as unknown as TResult1)); } // Insert / update / delete mutators — these are mostly used by legacy // auth flows and the AI preferences action. We capture the data and // short-circuit to a successful no-op response so the call sites // don't blow up. Real writes must go through server actions. executeSingle(): MockSingleResult { const result = this.execute(); if (Array.isArray(result.data) && result.data.length > 0) { return { data: result.data[0] as T, error: null }; } return { data: null, error: null }; } execute(): MockQueryResult { if (this.mode === "update" || this.mode === "delete") { return this.runMutation(); } // No mock data - return empty array let filtered: unknown[] = []; for (const filter of this.filters) { const inSet = filter.op === "in" && Array.isArray(filter.value) ? new Set(filter.value as unknown[]) : null; const likeNeedle = filter.op === "like" && typeof filter.value === "string" ? (filter.value as string).replace(/%/g, "") : null; const ilikeNeedle = filter.op === "ilike" && typeof filter.value === "string" ? (filter.value as string).replace(/%/g, "").toLowerCase() : null; filtered = filtered.filter((row) => { const r = row as Record; const rowValue = r[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" && likeNeedle !== null && likeNeedle.length > 0 && rowValue.includes(likeNeedle) ); case "ilike": return ( typeof rowValue === "string" && ilikeNeedle !== null && ilikeNeedle.length > 0 && rowValue.toLowerCase().includes(ilikeNeedle) ); case "in": return inSet !== null && inSet.has(rowValue); default: return true; } }); } if (this.orderColumn) { filtered.sort((a: unknown, b: unknown) => { const aVal = (a as Record)[this.orderColumn!] as string | number; const bVal = (b as Record)[this.orderColumn!] as string | number; if (aVal < bVal) return this.orderDirection === "desc" ? 1 : -1; if (aVal > bVal) return this.orderDirection === "desc" ? -1 : 1; return 0; }); } if (this.rangeMin !== undefined && this.rangeMax !== undefined) { filtered = filtered.slice(this.rangeMin, this.rangeMax + 1); } else if (this.limitValue !== undefined) { filtered = filtered.slice(0, this.limitValue); } return { data: filtered as T[], error: null }; } private runMutation(): MockQueryResult { if (this.mode === "delete") { return { data: null, error: null }; } if (this.mutationData) { const items = [this.mutationData]; const returning = items.map((item, i) => ({ ...item, id: (item as Record).id ?? `generated-${Date.now()}-${i}`, })); return { data: returning as unknown as T[], error: null }; } return { data: null, error: null }; } } interface MockMutationResult { data: T[] | null; error: null; } class MockMutationBuilder = Record> { private tableName: string; private data: unknown; constructor(tableName: string, data: unknown) { this.tableName = tableName; this.data = data; } select() { return new MockQueryBuilder(this.tableName); } eq() { return this; } then, TResult2 = never>( onfulfilled?: ((value: MockMutationResult) => TResult1 | PromiseLike) | null, ): PromiseLike { const result: MockMutationResult = { data: Array.isArray(this.data) ? this.data.map((item: Record, i: number) => ({ ...item, id: item?.id ?? `generated-${Date.now()}-${i}`, })) as unknown as T[] : this.data ? [{ ...(this.data as Record), id: (this.data as Record).id ?? `generated-${Date.now()}` }] as unknown as T[] : null, error: null, }; return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as unknown as TResult1)); } } class MockDeleteBuilder { eq(_column: string, _value: unknown) { return this; } in(_column: string, _values: unknown[]) { return this; } then(resolve: (value: unknown) => void) { resolve({ data: null, error: null }); } } class MockStorageBuilder { from(_bucket: string) { return { upload: async (_path: string, _file: unknown) => ({ data: { path: _path }, error: null }), download: async (_path: string) => ({ data: new Blob(), error: null }), remove: async (_paths: string[]) => ({ data: { paths: _paths }, error: null }), list: async () => ({ data: [], error: null }), }; } } // Auth credential types interface SignInCredentials { email?: string; password?: string; } interface UserAttributes { email?: string; data?: Record; } function createMockClient() { // The query builder is mutable, so we can't simply return a class // instance + spread mutation methods. The cleanest way to support // both `.from(t).select(...)...` (read) and `.from(t).update(d).eq(...)` // (write) in a single chain is to delegate everything to one object // and inspect `this.mode` lazily. We build that object via // `Object.assign` to keep the TypeScript inference happy. function makeFrom = Record>(table: string) { const qb = new MockQueryBuilder(table); return Object.assign(qb, { insert: (data: Record) => new MockMutationBuilder(table, data), update: (data: Record) => new MockMutationBuilder(table, data), upsert: (data: Record) => new MockMutationBuilder(table, data), delete: () => new MockDeleteBuilder(), }); } return { from: makeFrom, storage: new MockStorageBuilder(), auth: { getSession: async () => ({ data: { session: null }, error: null }), getUser: async () => ({ data: { user: null }, error: null }), signInWithPassword: async (_creds: SignInCredentials) => ({ data: { user: null, session: null }, error: null, }), signOut: async () => ({ error: null }), updateUser: async (_attrs: UserAttributes): Promise<{ data: { user: null }; error: null }> => ({ data: { user: null }, error: null, }), onAuthStateChange: () => ({ data: { subscription: { unsubscribe: () => {} } }, }), }, rpc: async (_name: string, _params?: Record): Promise<{ data: null; error: null }> => ({ data: null, error: null, }), channel: () => ({ on: () => ({ subscribe: () => ({}) }), subscribe: () => ({}), }), }; } export const supabase = createMockClient();