refactor(storefront): remove supabase shim and restore customer storefronts

The legacy src/lib/supabase.ts shim returned { data: null } for every
query, so 15+ pages were silently rendering empty state — including the
two customer-facing storefronts (tuxedo, indian-river-direct) and
several v1 admin pages. Replace every caller with canonical access:

- New src/actions/storefront.ts server-action module: brand lookup,
  public stops, active products, stop-by-slug, wholesale settings,
  portal config. Uses the shared pg Pool and SECURITY DEFINER RPCs.
- src/actions/brand-settings.ts: getBrandSettingsPublic inlined the
  brands + brand_settings LEFT JOIN wholesale_settings query (the
  RPC it called did not exist; try/catch was masking the failure).
- src/actions/ai/preferences.ts: switched get/saveAIPreferences to
  pool.query.
- src/actions/square-sync-ui.ts: new getSquareQueueCount action for
  SquareSyncWidget (replaces shim count).
- src/app/api/{tuxedo,indian-river-direct}/schedule-pdf/route.ts:
  use pool.query.
- next.config.ts: redirects /admin/{products/:id,reports,taxes,
  settings/{shipping,integrations,billing}} → their v2 / settings
  equivalents, then deleted those pages.
- Deleted src/lib/supabase.ts (no remaining imports).

Tests: 174/175 (unchanged from baseline; pre-existing getAdminUser
mock issue is tracked in Step 5).
This commit is contained in:
Nora
2026-06-25 17:12:28 -06:00
parent 9f3dc9b68e
commit 2daa8fd4b6
22 changed files with 241 additions and 1146 deletions
-372
View File
@@ -1,372 +0,0 @@
/**
* 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<T> {
data: T[] | null;
error: null;
}
interface MockSingleResult<T> {
data: T | null;
error: null;
}
class MockQueryBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
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<string, unknown> | null = null;
constructor(tableName: string) {
this.tableName = tableName;
// No mock data - return empty results
}
select(columns: string = "*", _opts?: Record<string, unknown>) {
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<MockSingleResult<T>> so destructured binding
// patterns in callers work under `--strict` (no implicit `any`).
single(): Promise<MockSingleResult<T>> {
return Promise.resolve(this.executeSingle());
}
maybeSingle(): Promise<MockSingleResult<T>> {
return Promise.resolve(this.executeSingle());
}
then<TResult1 = MockQueryResult<T>, TResult2 = never>(
onfulfilled?: ((value: MockQueryResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
_onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): PromiseLike<TResult1 | TResult2> {
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<T> {
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<T> {
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) {
filtered = filtered.filter((row) => {
const r = row as Record<string, unknown>;
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" &&
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;
}
});
}
if (this.orderColumn) {
filtered.sort((a: unknown, b: unknown) => {
const aVal = (a as Record<string, unknown>)[this.orderColumn!] as string | number;
const bVal = (b as Record<string, unknown>)[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<T> {
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<string, unknown>).id ?? `generated-${Date.now()}-${i}`,
}));
return { data: returning as unknown as T[], error: null };
}
return { data: null, error: null };
}
}
interface MockMutationResult<T> {
data: T[] | null;
error: null;
}
class MockMutationBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
private tableName: string;
private data: unknown;
constructor(tableName: string, data: unknown) {
this.tableName = tableName;
this.data = data;
}
select() {
return new MockQueryBuilder<T>(this.tableName);
}
eq() {
return this;
}
then<TResult1 = MockMutationResult<T>, TResult2 = never>(
onfulfilled?: ((value: MockMutationResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
): PromiseLike<TResult1 | TResult2> {
const result: MockMutationResult<T> = {
data: Array.isArray(this.data)
? this.data.map((item: Record<string, unknown>, i: number) => ({
...item,
id: item?.id ?? `generated-${Date.now()}-${i}`,
})) as unknown as T[]
: this.data
? [{ ...(this.data as Record<string, unknown>), id: (this.data as Record<string, unknown>).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<string, unknown>;
}
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<T extends Record<string, unknown> = Record<string, unknown>>(table: string) {
const qb = new MockQueryBuilder<T>(table);
return Object.assign(qb, {
insert: (data: Record<string, unknown>) => new MockMutationBuilder<T>(table, data),
update: (data: Record<string, unknown>) => new MockMutationBuilder<T>(table, data),
upsert: (data: Record<string, unknown>) => new MockMutationBuilder<T>(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<string, unknown>): Promise<{ data: null; error: null }> => ({
data: null,
error: null,
}),
channel: () => ({
on: () => ({ subscribe: () => ({}) }),
subscribe: () => ({}),
}),
};
}
export const supabase = createMockClient();