feat(storage): MinIO object storage, Neon Auth, Supabase removal
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
Deploy to route.crispygoat.com / deploy (push) Failing after 3m1s
- Add MinIO/S3-compatible storage client (src/lib/storage.ts) with uploadObject, deleteObject, presigned URL helpers, and BUCKETS constant - Wire product images, brand logos, and water log photos to MinIO via the new storage client - Migrate forgot-password to Neon Auth (remove Supabase /auth/v1/recover call) - Migrate send-scheduled cron to direct Postgres + Resend (remove Supabase Edge Function proxy) - Add logoUrl to email types (OrderReceipt, Welcome, PasswordReset) and pass brand_settings.logo_url from all call sites - Update email templates to use dynamic logoUrl instead of hardcoded Supabase bucket URLs - Remove hardcoded Supabase URLs from TuxedoVideoHero, TuxedoAboutPage, TimeTrackingFieldClient; use brand_settings props + local public/ fallback - Download brand logos (3) and tuxedo-hero.mp4 (36MB) from Supabase bucket to public/ for local development - Add MinIO env vars to .env.example (endpoint, access key, secret, buckets) - Fix TimeTrackingFieldClient to destructure logoUrl and brandAccent props - Fix admin/users.ts logoUrl type (null → undefined for optional string) - Remove stale sb- cookie from wholesale-auth - Migrate tuxedo/about page to remove supabase import and use pool query for wholesale_settings lookup
This commit is contained in:
+72
-74
@@ -10,7 +10,7 @@
|
||||
*
|
||||
* 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` / `withTenant`.
|
||||
* 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).
|
||||
@@ -29,16 +29,21 @@
|
||||
* If a call site needs more than that, migrate the call site.
|
||||
*/
|
||||
|
||||
import { getMockTableData } from "./mock-data";
|
||||
|
||||
const useMockData =
|
||||
process.env.NEXT_PUBLIC_USE_MOCK_DATA === "true" || true;
|
||||
|
||||
type FilterOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "is" | "like" | "ilike" | "in";
|
||||
type Filter = { column: string; value: unknown; op: FilterOp };
|
||||
|
||||
class MockQueryBuilder {
|
||||
private data: unknown[];
|
||||
// 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 = "*";
|
||||
@@ -52,11 +57,10 @@ class MockQueryBuilder {
|
||||
|
||||
constructor(tableName: string) {
|
||||
this.tableName = tableName;
|
||||
this.data = [...(getMockTableData(tableName) || [])];
|
||||
// No mock data - return empty results
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
select(columns: string = "*", _opts?: any) {
|
||||
select(columns: string = "*", _opts?: Record<string, unknown>) {
|
||||
this.selectColumns = columns;
|
||||
return this;
|
||||
}
|
||||
@@ -131,55 +135,47 @@ class MockQueryBuilder {
|
||||
// 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<{ data: any, error: any }> so destructured binding
|
||||
// proper Promise<MockSingleResult<T>> so destructured binding
|
||||
// patterns in callers work under `--strict` (no implicit `any`).
|
||||
single() {
|
||||
single(): Promise<MockSingleResult<T>> {
|
||||
return Promise.resolve(this.executeSingle());
|
||||
}
|
||||
|
||||
maybeSingle() {
|
||||
maybeSingle(): Promise<MockSingleResult<T>> {
|
||||
return Promise.resolve(this.executeSingle());
|
||||
}
|
||||
|
||||
// Return a generic `any` to match the historical Supabase client
|
||||
// typing (`data: T[]`, `data: T` for `.single()`). Without this, every
|
||||
// consumer would have to be rewritten just to satisfy the type
|
||||
// checker, which defeats the purpose of the shim.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
then<TResult1 = any, TResult2 = never>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
_onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
then<TResult1 = MockQueryResult<T>, TResult2 = never>(
|
||||
onfulfilled?: ((value: MockQueryResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
_onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
|
||||
): PromiseLike<TResult1 | TResult2> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = this.execute();
|
||||
return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as TResult1));
|
||||
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.
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
executeSingle(): any {
|
||||
executeSingle(): MockSingleResult<T> {
|
||||
const result = this.execute();
|
||||
if (Array.isArray(result.data) && result.data.length > 0) {
|
||||
return { data: result.data[0], error: null };
|
||||
return { data: result.data[0] as T, error: null };
|
||||
}
|
||||
return { data: null, error: null };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
execute(): any {
|
||||
execute(): MockQueryResult<T> {
|
||||
if (this.mode === "update" || this.mode === "delete") {
|
||||
return this.runMutation();
|
||||
}
|
||||
let filtered: unknown[] = [...this.data];
|
||||
// No mock data - return empty array
|
||||
let filtered: unknown[] = [];
|
||||
|
||||
for (const filter of this.filters) {
|
||||
filtered = filtered.filter((row: any) => {
|
||||
const rowValue = row[filter.column];
|
||||
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;
|
||||
@@ -218,10 +214,9 @@ class MockQueryBuilder {
|
||||
}
|
||||
|
||||
if (this.orderColumn) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
filtered.sort((a: any, b: any) => {
|
||||
const aVal = a[this.orderColumn!];
|
||||
const bVal = b[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;
|
||||
@@ -234,11 +229,10 @@ class MockQueryBuilder {
|
||||
filtered = filtered.slice(0, this.limitValue);
|
||||
}
|
||||
|
||||
return { data: filtered, error: null };
|
||||
return { data: filtered as T[], error: null };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private runMutation(): any {
|
||||
private runMutation(): MockQueryResult<T> {
|
||||
if (this.mode === "delete") {
|
||||
return { data: null, error: null };
|
||||
}
|
||||
@@ -248,16 +242,20 @@ class MockQueryBuilder {
|
||||
...item,
|
||||
id: (item as Record<string, unknown>).id ?? `generated-${Date.now()}-${i}`,
|
||||
}));
|
||||
return { data: returning, error: null };
|
||||
return { data: returning as unknown as T[], error: null };
|
||||
}
|
||||
return { data: null, error: null };
|
||||
}
|
||||
}
|
||||
|
||||
class MockMutationBuilder {
|
||||
interface MockMutationResult<T> {
|
||||
data: T[] | null;
|
||||
error: null;
|
||||
}
|
||||
|
||||
class MockMutationBuilder<T extends Record<string, unknown> = Record<string, unknown>> {
|
||||
private tableName: string;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private data: any;
|
||||
private data: unknown;
|
||||
|
||||
constructor(tableName: string, data: unknown) {
|
||||
this.tableName = tableName;
|
||||
@@ -265,31 +263,28 @@ class MockMutationBuilder {
|
||||
}
|
||||
|
||||
select() {
|
||||
return new MockQueryBuilder(this.tableName);
|
||||
return new MockQueryBuilder<T>(this.tableName);
|
||||
}
|
||||
|
||||
eq() {
|
||||
return this;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
then<TResult1 = any, TResult2 = never>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onfulfilled?: ((value: any) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
then<TResult1 = MockMutationResult<T>, TResult2 = never>(
|
||||
onfulfilled?: ((value: MockMutationResult<T>) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
): PromiseLike<TResult1 | TResult2> {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const result: any = {
|
||||
const result: MockMutationResult<T> = {
|
||||
data: Array.isArray(this.data)
|
||||
? this.data.map((item: any, i: number) => ({
|
||||
? 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, id: this.data.id ?? `generated-${Date.now()}` }]
|
||||
? [{ ...(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 TResult1));
|
||||
return Promise.resolve(onfulfilled ? onfulfilled(result) : (result as unknown as TResult1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +311,17 @@ class MockStorageBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -323,15 +329,12 @@ function createMockClient() {
|
||||
// (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(table: string) {
|
||||
const qb = new MockQueryBuilder(table);
|
||||
function makeFrom<T extends Record<string, unknown> = Record<string, unknown>>(table: string) {
|
||||
const qb = new MockQueryBuilder<T>(table);
|
||||
return Object.assign(qb, {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
insert: (data: any) => new MockMutationBuilder(table, data),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
update: (data: any) => new MockMutationBuilder(table, data),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
upsert: (data: any) => new MockMutationBuilder(table, data),
|
||||
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(),
|
||||
});
|
||||
}
|
||||
@@ -342,14 +345,12 @@ function createMockClient() {
|
||||
auth: {
|
||||
getSession: async () => ({ data: { session: null }, error: null }),
|
||||
getUser: async () => ({ data: { user: null }, error: null }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
signInWithPassword: async (_creds: any) => ({
|
||||
signInWithPassword: async (_creds: SignInCredentials) => ({
|
||||
data: { user: null, session: null },
|
||||
error: null,
|
||||
}),
|
||||
signOut: async () => ({ error: null }),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
updateUser: async (_attrs: any): Promise<any> => ({
|
||||
updateUser: async (_attrs: UserAttributes): Promise<{ data: { user: null }; error: null }> => ({
|
||||
data: { user: null },
|
||||
error: null,
|
||||
}),
|
||||
@@ -357,8 +358,7 @@ function createMockClient() {
|
||||
data: { subscription: { unsubscribe: () => {} } },
|
||||
}),
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
rpc: async (_name: string, _params?: any): Promise<any> => ({
|
||||
rpc: async (_name: string, _params?: Record<string, unknown>): Promise<{ data: null; error: null }> => ({
|
||||
data: null,
|
||||
error: null,
|
||||
}),
|
||||
@@ -369,6 +369,4 @@ function createMockClient() {
|
||||
};
|
||||
}
|
||||
|
||||
export const supabase = useMockData ? createMockClient() : createMockClient();
|
||||
|
||||
export { useMockData };
|
||||
export const supabase = createMockClient();
|
||||
|
||||
Reference in New Issue
Block a user