| import { createServerClient, createServiceClient } from 'lib/supabase/server' |
| import { randomUUID } from 'crypto' |
| import type { Database } from 'types/database' |
| import type { SupabaseClient } from '@supabase/supabase-js' |
| import { Pool } from 'pg' |
| import { promises as fs } from 'fs' |
| import path from 'path' |
| import { logEvent } from './logger' |
| import { allowInMemoryFallback } from './supabase/env' |
| import { isSupabaseConfigured } from './supabase/env' |
|
|
| const LOCAL_DB_URL = process.env.LOCAL_DATABASE_URL |
| const FILE_STORE_PATH = process.env.LOCAL_FILE_STORE_PATH || path.join(process.cwd(), '.data', 'store.json') |
| const ENABLE_FILE_STORE = process.env.LOCAL_FILE_STORE === '1' |
|
|
| let pool: Pool | null = null |
| if (LOCAL_DB_URL) { |
| pool = new Pool({ connectionString: LOCAL_DB_URL }) |
| } |
|
|
| |
| const inMemory = { |
| codes: [] as Array<Record<string, any>>, |
| rooms: [] as Array<Record<string, any>>, |
| room_messages: [] as Array<Record<string, any>>, |
| logs: [] as Array<Record<string, any>> |
| } |
|
|
| function isDevFallbackAllowed() { |
| |
| if (allowInMemoryFallback) return true |
| return process.env.NODE_ENV !== 'production' |
| } |
|
|
| type FileStore = { |
| codes: Array<Record<string, any>> |
| rooms: Array<Record<string, any>> |
| room_messages: Array<Record<string, any>> |
| } |
|
|
| async function ensureFileStoreDir() { |
| const dir = path.dirname(FILE_STORE_PATH) |
| await fs.mkdir(dir, { recursive: true }) |
| } |
|
|
| async function readFileStore(): Promise<FileStore> { |
| if (!ENABLE_FILE_STORE) return { codes: [], rooms: [], room_messages: [] } |
| try { |
| const buf = await fs.readFile(FILE_STORE_PATH, 'utf8') |
| return JSON.parse(buf) as FileStore |
| } catch { |
| return { codes: [], rooms: [], room_messages: [] } |
| } |
| } |
|
|
| async function writeFileStore(store: FileStore) { |
| if (!ENABLE_FILE_STORE) return |
| await ensureFileStoreDir() |
| await fs.writeFile(FILE_STORE_PATH, JSON.stringify(store, null, 2), 'utf8') |
| } |
|
|
| async function getSupabase(): Promise<SupabaseClient<Database>> { |
| return createServerClient() |
| } |
|
|
| |
| async function getSupabaseWrite(): Promise<SupabaseClient<Database>> { |
| if (process.env.SUPABASE_SERVICE_ROLE_KEY) { |
| return createServiceClient() |
| } |
| return createServerClient() |
| } |
|
|
| function pickFallbackStore() { |
| |
| return ENABLE_FILE_STORE ? readFileStore() : Promise.resolve({ codes: inMemory.codes, rooms: inMemory.rooms, room_messages: inMemory.room_messages }) |
| } |
|
|
| |
|
|
| |
| |
| export async function getCodeWithLinked(codeId: string) { |
| const primary = await getCodeById(codeId) |
| if (!primary) return { primary: null, linked: null } |
|
|
| |
| let linked = primary.linked_to ? await getCodeById(primary.linked_to) : null |
| if (!linked) { |
| if (pool) { |
| const res = await pool.query('SELECT * FROM codes WHERE linked_to = $1 LIMIT 1', [codeId]) |
| linked = res.rows[0] || null |
| } else { |
| try { |
| const supabase = await getSupabase() |
| const { data } = await supabase.from('codes').select('*').eq('linked_to', codeId).limit(1) |
| linked = data && data.length > 0 ? (data[0] as any) : null |
| } catch (e) { |
| console.error('[persistence.getCodeWithLinked] supabase error (reverse lookup):', e) |
| if (isDevFallbackAllowed()) { |
| linked = inMemory.codes.find((c) => c.linked_to === codeId) || null |
| } |
| } |
| } |
| } |
|
|
| return { primary, linked } |
| } |
|
|
| export async function getCodeById(codeId: string) { |
| if (pool) { |
| const res = await pool.query('SELECT * FROM codes WHERE id = $1 LIMIT 1', [codeId]) |
| return res.rows[0] || null |
| } |
| try { |
| const supabase = await getSupabase() |
| const { data } = await supabase.from('codes').select('*').eq('id', codeId).single() |
| return data || null |
| } catch (e) { |
| console.error('[persistence.getCodeById] supabase error:', e) |
| if (isDevFallbackAllowed()) { |
| return inMemory.codes.find((c) => c.id === codeId) || null |
| } |
| return null |
| } |
| } |
|
|
| export async function getCodeByCode(code: string) { |
| if (pool) { |
| const res = await pool.query('SELECT * FROM codes WHERE code = $1 LIMIT 1', [code.toUpperCase()]) |
| return res.rows[0] || null |
| } |
| try { |
| const supabase = await getSupabase() |
| const { data } = await supabase.from('codes').select('*').eq('code', code.toUpperCase()).single() |
| return data || null |
| } catch (e) { |
| console.error('[persistence.getCodeByCode] supabase error:', e) |
| if (isDevFallbackAllowed()) { |
| return inMemory.codes.find((c) => c.code === code.toUpperCase()) || null |
| } |
| return null |
| } |
| } |
|
|
| export async function getCodeForUserInRoom(roomId: string, userId: string) { |
| if (pool) { |
| const res = await pool.query('SELECT id FROM codes WHERE room_id = $1 AND user_id = $2 LIMIT 1', [roomId, userId]) |
| return res.rows[0] || null |
| } |
| try { |
| const supabase = await getSupabaseWrite() |
| const { data } = await supabase.from('codes').select('id').eq('room_id', roomId).eq('user_id', userId).limit(1) |
| return data && data.length > 0 ? data[0] : null |
| } catch (e) { |
| console.error('[persistence.getCodeForUserInRoom] supabase error:', e) |
| if (isDevFallbackAllowed()) { |
| return inMemory.codes.find((c) => c.room_id === roomId && c.user_id === userId) || null |
| } |
| return null |
| } |
| } |
|
|
| export async function getAnyCodeForRoom(roomId: string) { |
| if (pool) { |
| const res = await pool.query('SELECT id FROM codes WHERE room_id = $1 LIMIT 1', [roomId]) |
| return res.rows[0] || null |
| } |
| try { |
| const supabase = await getSupabaseWrite() |
| const { data } = await supabase.from('codes').select('id').eq('room_id', roomId).limit(1) |
| return data && data.length > 0 ? data[0] : null |
| } catch (e) { |
| console.error('[persistence.getAnyCodeForRoom] supabase error:', e) |
| if (isDevFallbackAllowed()) { |
| return inMemory.codes.find((c) => c.room_id === roomId) || null |
| } |
| return null |
| } |
| } |
|
|
| export async function insertCode(payload: Database['public']['Tables']['codes']['Insert']) { |
| const normalizedPayload: Database['public']['Tables']['codes']['Insert'] = { |
| ...payload, |
| code: payload.code.toUpperCase() |
| } |
|
|
| if (pool) { |
| const cols = Object.keys(normalizedPayload) |
| const vals = Object.values(normalizedPayload) |
| const idx = vals.map((_, i) => `$${i + 1}`).join(',') |
| const query = `INSERT INTO codes(${cols.join(',')}) VALUES(${idx}) RETURNING *` |
| const res = await pool.query(query, vals) |
| await logEvent({ module: 'codes', operation: 'insert', data: res.rows[0] }) |
| return res.rows[0] |
| } |
| const supabase = await getSupabaseWrite() |
| try { |
| const { data, error } = await supabase.from('codes').insert(normalizedPayload).select().single() |
| if (error) { |
| console.error('[persistence.insertCode] anon insert error:', error) |
| |
| try { |
| if (process.env.SUPABASE_SERVICE_ROLE_KEY) { |
| const service = createServiceClient() |
| const { data: sdata, error: serror } = await service.from('codes').insert(normalizedPayload).select().single() |
| if (serror) { |
| console.error('[persistence.insertCode] service insert error:', serror) |
| |
| } else { |
| const row = sdata || null |
| await logEvent({ module: 'codes', operation: 'insert', data: row }) |
| return row |
| } |
| } |
| } catch (svcErr) { |
| console.error('[persistence.insertCode] service client attempt failed:', svcErr) |
| } |
| |
| if (isDevFallbackAllowed()) { |
| const id = randomUUID() |
| const now = new Date().toISOString() |
| const row = { |
| id, |
| code: normalizedPayload.code, |
| linked_to: normalizedPayload.linked_to || null, |
| used: normalizedPayload.used ?? null, |
| user_id: normalizedPayload.user_id || null, |
| room_id: normalizedPayload.room_id || null, |
| pin_hash: normalizedPayload.pin_hash || null, |
| session_hash: normalizedPayload.session_hash || null, |
| date_first: normalizedPayload.date_first || null, |
| date_last: normalizedPayload.date_last || null, |
| used_count: normalizedPayload.used_count ?? 0, |
| created_at: normalizedPayload.created_at || now |
| } |
| inMemory.codes.push(row) |
| console.warn('[persistence.insertCode] falling back to in-memory storage (dev only)') |
| await logEvent({ module: 'codes', operation: 'insert_fallback', data: row, error: error?.message || null }) |
| return row |
| } |
| return null |
| } |
| const row = data || null |
| await logEvent({ module: 'codes', operation: 'insert', data: row }) |
| return row |
| } catch (e) { |
| console.error('[persistence.insertCode] unexpected error:', e) |
| if (isDevFallbackAllowed()) { |
| const id = randomUUID() |
| const now = new Date().toISOString() |
| const row = { |
| id, |
| code: normalizedPayload.code, |
| linked_to: normalizedPayload.linked_to || null, |
| used: normalizedPayload.used ?? null, |
| user_id: normalizedPayload.user_id || null, |
| room_id: normalizedPayload.room_id || null, |
| pin_hash: normalizedPayload.pin_hash || null, |
| session_hash: normalizedPayload.session_hash || null, |
| date_first: normalizedPayload.date_first || null, |
| date_last: normalizedPayload.date_last || null, |
| used_count: normalizedPayload.used_count ?? 0, |
| created_at: normalizedPayload.created_at || now |
| } |
| inMemory.codes.push(row) |
| console.warn('[persistence.insertCode] unexpected error - falling back to in-memory (dev only)') |
| await logEvent({ module: 'codes', operation: 'insert_fallback', data: row, error: (e as Error)?.message || String(e) }) |
| return row |
| } |
| return null |
| } |
| } |
|
|
| export async function updateCode(id: string, payload: Partial<Database['public']['Tables']['codes']['Update']>) { |
| if (pool) { |
| const cols = Object.keys(payload) |
| const vals = Object.values(payload) |
| const set = cols.map((c, i) => `${c} = $${i + 1}`).join(',') |
| const query = `UPDATE codes SET ${set} WHERE id = $${cols.length + 1} RETURNING *` |
| const res = await pool.query(query, [...vals, id]) |
| await logEvent({ module: 'codes', operation: 'update', data: res.rows[0] }) |
| return res.rows[0] |
| } |
| const supabase = await getSupabaseWrite() |
| try { |
| const { data, error } = await supabase.from('codes').update(payload).eq('id', id).select().single() |
| if (error) { |
| console.error('[persistence.updateCode] supabase update error:', error) |
| if (isDevFallbackAllowed()) { |
| const idx = inMemory.codes.findIndex((c) => c.id === id) |
| if (idx >= 0) { |
| inMemory.codes[idx] = { ...inMemory.codes[idx], ...payload } |
| await logEvent({ module: 'codes', operation: 'update_fallback', data: inMemory.codes[idx], error: error.message || null }) |
| return inMemory.codes[idx] |
| } |
| } |
| return null |
| } |
| const row = data || null |
| await logEvent({ module: 'codes', operation: 'update', data: row }) |
| return row |
| } catch (e) { |
| console.error('[persistence.updateCode] supabase error (exception):', e) |
| if (isDevFallbackAllowed()) { |
| const idx = inMemory.codes.findIndex((c) => c.id === id) |
| if (idx >= 0) { |
| inMemory.codes[idx] = { ...inMemory.codes[idx], ...payload } |
| await logEvent({ module: 'codes', operation: 'update_fallback', data: inMemory.codes[idx], error: (e as Error)?.message || String(e) }) |
| return inMemory.codes[idx] |
| } |
| } |
| return null |
| } |
| } |
|
|
| |
| export async function insertRoom(payload: Partial<Database['public']['Tables']['rooms']['Insert']>) { |
| if (pool) { |
| const cols = Object.keys(payload) |
| const vals = Object.values(payload) |
| const idx = vals.map((_, i) => `$${i + 1}`).join(',') |
| const query = `INSERT INTO rooms(${cols.join(',')}) VALUES(${idx}) RETURNING *` |
| const res = await pool.query(query, vals) |
| await logEvent({ module: 'rooms', operation: 'insert', data: res.rows[0] }) |
| return res.rows[0] |
| } |
| try { |
| const supabase = await getSupabaseWrite() |
| const { data, error } = await supabase.from('rooms').insert(payload).select().single() |
| if (error) { |
| console.error('[persistence.insertRoom] supabase error:', error) |
| if (isDevFallbackAllowed()) { |
| const id = randomUUID() |
| const now = new Date().toISOString() |
| const row = { |
| id, |
| name: payload.name || null, |
| description: payload.description || null, |
| status: payload.status ?? 1, |
| date_created: payload.date_created || now, |
| date_last_message: payload.date_last_message || null, |
| updated_at: payload.updated_at || now |
| } |
| inMemory.rooms.push(row) |
| console.warn('[persistence.insertRoom] falling back to in-memory storage (dev only)') |
| await logEvent({ module: 'rooms', operation: 'insert_fallback', data: row, error: error.message || null }) |
| return row |
| } |
| return null |
| } |
| await logEvent({ module: 'rooms', operation: 'insert', data: data || null }) |
| return data || null |
| } catch (e) { |
| console.error('[persistence.insertRoom] supabase error:', e) |
| if (isDevFallbackAllowed()) { |
| const id = randomUUID() |
| const now = new Date().toISOString() |
| const row = { |
| id, |
| name: payload.name || null, |
| description: payload.description || null, |
| status: payload.status ?? 1, |
| date_created: payload.date_created || now, |
| date_last_message: payload.date_last_message || null, |
| updated_at: payload.updated_at || now |
| } |
| inMemory.rooms.push(row) |
| console.warn('[persistence.insertRoom] falling back to in-memory storage (dev only)') |
| await logEvent({ module: 'rooms', operation: 'insert_fallback', data: row, error: (e as Error)?.message || String(e) }) |
| return row |
| } |
| return null |
| } |
| } |
|
|
| export async function getRoomById(roomId: string) { |
| if (pool) { |
| try { |
| const res = await pool.query('SELECT * FROM rooms WHERE id = $1 LIMIT 1', [roomId]) |
| console.log('[persistence.getRoomById] using pool, found=', !!res.rows[0]) |
| return res.rows[0] || null |
| } catch (e) { |
| console.error('[persistence.getRoomById] pool error:', e) |
| } |
| } |
| try { |
| console.log('[persistence.getRoomById] no pool, isSupabaseConfigured=', isSupabaseConfigured()) |
| const supabase = await getSupabaseWrite() |
| const { data } = await supabase.from('rooms').select('*').eq('id', roomId).single() |
| console.log('[persistence.getRoomById] supabase returned=', !!data) |
| return data || null |
| } catch (e) { |
| console.error('[persistence.getRoomById] supabase error:', e) |
| if (isDevFallbackAllowed()) { |
| const found = inMemory.rooms.find((r) => r.id === roomId) || null |
| console.log('[persistence.getRoomById] falling back to inMemory, found=', !!found) |
| return found |
| } |
| return null |
| } |
| } |
|
|
| export async function updateRoom(roomId: string, payload: Partial<Database['public']['Tables']['rooms']['Update']>) { |
| if (pool) { |
| const cols = Object.keys(payload) |
| const vals = Object.values(payload) |
| const set = cols.map((c, i) => `${c} = $${i + 1}`).join(',') |
| const query = `UPDATE rooms SET ${set} WHERE id = $${cols.length + 1} RETURNING *` |
| const res = await pool.query(query, [...vals, roomId]) |
| await logEvent({ module: 'rooms', operation: 'update', data: res.rows[0] }) |
| return res.rows[0] |
| } |
| try { |
| const supabase = await getSupabaseWrite() |
| const { data, error } = await supabase.from('rooms').update(payload).eq('id', roomId).select().single() |
| if (error) { |
| console.error('[persistence.updateRoom] supabase error:', error) |
| if (isDevFallbackAllowed()) { |
| const idx = inMemory.rooms.findIndex((r) => r.id === roomId) |
| if (idx >= 0) { |
| inMemory.rooms[idx] = { ...inMemory.rooms[idx], ...payload } |
| await logEvent({ module: 'rooms', operation: 'update_fallback', data: inMemory.rooms[idx], error: error.message || null }) |
| return inMemory.rooms[idx] |
| } |
| } |
| return null |
| } |
| const row = data || null |
| await logEvent({ module: 'rooms', operation: 'update', data: row }) |
| return row |
| } catch (e) { |
| console.error('[persistence.updateRoom] supabase error (exception):', e) |
| if (isDevFallbackAllowed()) { |
| const idx = inMemory.rooms.findIndex((r) => r.id === roomId) |
| if (idx >= 0) { |
| inMemory.rooms[idx] = { ...inMemory.rooms[idx], ...payload } |
| await logEvent({ module: 'rooms', operation: 'update_fallback', data: inMemory.rooms[idx], error: (e as Error)?.message || String(e) }) |
| return inMemory.rooms[idx] |
| } |
| } |
| return null |
| } |
| } |
|
|
| export async function updateCodesRoomId(codeIds: string[], roomId: string) { |
| if (pool) { |
| const query = `UPDATE codes SET room_id = $1 WHERE id = ANY($2::uuid[]) RETURNING id, room_id` |
| const res = await pool.query(query, [roomId, codeIds]) |
| await logEvent({ module: 'codes', operation: 'update_room', data: res.rows }) |
| return res.rows |
| } |
| try { |
| const supabase = await getSupabase() |
| const { data } = await supabase.from('codes').update({ room_id: roomId }).in('id', codeIds).select('id, room_id') |
| return data || [] |
| } catch (e) { |
| console.error('[persistence.updateCodesRoomId] supabase error:', e) |
| if (isDevFallbackAllowed()) { |
| const updated: Array<{ id: string; room_id: string } > = [] |
| for (const id of codeIds) { |
| const idx = inMemory.codes.findIndex((c) => c.id === id) |
| if (idx >= 0) { |
| inMemory.codes[idx].room_id = roomId |
| updated.push({ id, room_id: roomId }) |
| } |
| } |
| await logEvent({ module: 'codes', operation: 'update_room_fallback', data: updated, error: (e as Error)?.message || String(e) }) |
| return updated |
| } |
| return [] |
| } |
| } |
|
|
| |
| type RoomWithCodes = { |
| id: string |
| name?: string | null |
| description?: string | null |
| status?: number | null |
| date_created: string |
| date_last_message?: string | null |
| message_count: number |
| codes: Array<{ id: string; code: string }> |
| } |
|
|
| export async function getRoomsForUser(userId: string): Promise<RoomWithCodes[]> { |
| if (!userId) return [] |
|
|
| |
| const aggregate = (rows: Array<Record<string, any>>): RoomWithCodes[] => { |
| const map = new Map<string, RoomWithCodes>() |
| for (const row of rows) { |
| const roomId = row.room_id || row.id |
| if (!roomId) continue |
| const existing: RoomWithCodes = map.get(roomId) || { |
| id: roomId, |
| name: row.name ?? null, |
| description: row.description ?? null, |
| status: row.status ?? 1, |
| date_created: row.date_created || row.created_at || new Date().toISOString(), |
| date_last_message: row.date_last_message || row.updated_at || null, |
| message_count: Number(row.message_count ?? 0) || 0, |
| codes: [] |
| } |
| if (row.code_id && row.code) { |
| existing.codes.push({ id: row.code_id, code: row.code }) |
| } else if (row.id && row.code) { |
| existing.codes.push({ id: row.id, code: row.code }) |
| } |
| map.set(roomId, existing) |
| } |
| return Array.from(map.values()) |
| } |
|
|
| |
| if (pool) { |
| const res = await pool.query( |
| ` |
| SELECT |
| r.*, |
| COALESCE(r.date_last_message, r.updated_at, r.date_created) AS date_last_message, |
| (SELECT COUNT(*) FROM room_messages m WHERE m.room_id = r.id) AS message_count, |
| c.id AS code_id, |
| c.code |
| FROM rooms r |
| LEFT JOIN codes c ON c.room_id = r.id |
| WHERE (c.user_id = $1 OR r.creator_id = $1) AND r.status = 1 |
| ORDER BY COALESCE(r.date_last_message, r.updated_at, r.date_created) DESC |
| `, |
| [userId] |
| ) |
| return aggregate(res.rows) |
| } |
|
|
| |
| if (isSupabaseConfigured()) { |
| try { |
| const supabase = await getSupabaseWrite() |
|
|
| |
| let rows: Array<Record<string, any>> = [] |
| try { |
| const { data: creatorRooms, error: creatorErr } = await supabase |
| .from('rooms') |
| .select('*, room_messages(count)') |
| .eq('creator_id', userId) |
| .eq('status', 1) |
| if (creatorErr) { |
| console.error('[persistence.getRoomsForUser] supabase creator rooms error:', creatorErr) |
| } else if (creatorRooms && Array.isArray(creatorRooms) && creatorRooms.length > 0) { |
| rows = creatorRooms.map((r: any) => ({ |
| ...r, |
| room_id: r.id, |
| message_count: r.room_messages?.[0]?.count ?? 0 |
| })) |
| } |
| } catch (e) { |
| console.error('[persistence.getRoomsForUser] supabase creator rooms exception:', e) |
| } |
|
|
| |
| try { |
| const { data: codesData, error: codesError } = await supabase |
| .from('codes') |
| .select('id, code, room_id') |
| .eq('user_id', userId) |
| .not('room_id', 'is', null) |
|
|
| if (codesError) { |
| console.error('[persistence.getRoomsForUser] supabase codes fetch error:', codesError) |
| } else if (codesData && Array.isArray(codesData) && codesData.length > 0) { |
| const roomIds = Array.from(new Set(codesData.map((c: any) => c.room_id).filter(Boolean))) |
| if (roomIds.length > 0) { |
| const { data: rdata, error: rerr } = await supabase |
| .from('rooms') |
| .select('*, room_messages(count)') |
| .in('id', roomIds) |
| .eq('status', 1) |
| if (rerr) { |
| console.error('[persistence.getRoomsForUser] supabase rooms by id error:', rerr) |
| } else if (rdata && Array.isArray(rdata)) { |
| const existingIds = new Set(rows.map((rr) => rr.room_id)) |
| for (const c of codesData) { |
| const r = rdata.find((x: any) => x.id === c.room_id) |
| if (!r) continue |
| const rid = (r as any).id |
| if (existingIds.has(rid)) continue |
| rows.push({ |
| ...(r as any), |
| room_id: rid, |
| code_id: c.id, |
| code: c.code, |
| message_count: (r as any).room_messages?.[0]?.count ?? 0 |
| }) |
| existingIds.add(rid) |
| } |
| } |
| } |
| } |
| } catch (e) { |
| console.error('[persistence.getRoomsForUser] supabase codes exception:', e) |
| } |
|
|
| console.log('[persistence.getRoomsForUser] supabase rows=', rows.length) |
| return aggregate(rows) |
| } catch (e) { |
| console.error('[persistence.getRoomsForUser] supabase threw:', e) |
| } |
| } |
|
|
| |
| if (isDevFallbackAllowed()) { |
| const store = await pickFallbackStore() |
| const rows: Array<Record<string, any>> = [] |
| for (const c of store.codes) { |
| if (c.user_id !== userId || !c.room_id) continue |
| const room = store.rooms.find((r) => r.id === c.room_id) |
| if (!room || room.status === 0) continue |
| const messageCount = store.room_messages.filter((m) => m.room_id === c.room_id).length |
| rows.push({ |
| ...room, |
| room_id: room.id, |
| code_id: c.id, |
| code: c.code, |
| message_count: messageCount |
| }) |
| } |
| return aggregate(rows) |
| } |
|
|
| return [] |
| } |
|
|
| |
| export async function syncLocalToSupabase() { |
| if (!pool) return { synced: 0 } |
| const supabaseUrl = process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL |
| if (!supabaseUrl || !process.env.SUPABASE_SERVICE_ROLE_KEY) { |
| console.error('Missing Supabase service credentials for sync; skipping push') |
| return { synced: 0 } |
| } |
| const supabase = await createServerClient({ service: true }) |
| let synced = 0 |
|
|
| |
| const roomsRes = await pool.query('SELECT * FROM rooms') |
| for (const r of roomsRes.rows) { |
| |
| const { data, error } = await supabase.from('rooms').upsert(r).select().single() |
| if (!error) synced++ |
| } |
|
|
| |
| const codesRes = await pool.query('SELECT * FROM codes') |
| for (const c of codesRes.rows) { |
| const { data, error } = await supabase.from('codes').upsert(c).select().single() |
| if (!error) synced++ |
| } |
|
|
| |
| const messagesRes = await pool.query('SELECT * FROM room_messages') |
| for (const m of messagesRes.rows) { |
| const { data, error } = await supabase.from('room_messages').upsert(m).select().single() |
| if (!error) synced++ |
| } |
|
|
| |
| return { synced } |
| } |
|
|
| export async function closePool() { |
| if (pool) await pool.end() |
| } |
|
|
| |
|
|
| async function upsertMessagesLocal(rows: Array<Record<string, any>>) { |
| if (!pool || rows.length === 0) return |
| for (const row of rows) { |
| const cols = Object.keys(row) |
| const vals = Object.values(row) |
| const colList = cols.map((c) => `"${c}"`).join(',') |
| const placeholders = cols.map((_, i) => `$${i + 1}`).join(',') |
| const updates = cols.map((c) => `"${c}" = EXCLUDED."${c}"`).join(',') |
| const query = `INSERT INTO room_messages (${colList}) VALUES(${placeholders}) ON CONFLICT (id) DO UPDATE SET ${updates}` |
| await pool.query(query, vals) |
| } |
| } |
|
|
| export async function syncRoomMessagesFromSupabase(roomId: string) { |
| if (!pool) return { synced: 0 } |
| if (!process.env.SUPABASE_SERVICE_ROLE_KEY) return { synced: 0 } |
| const supabase = await createServerClient({ service: true }) |
| const { rows: latestRows } = await pool.query('SELECT COALESCE(MAX(created_at), \'1970-01-01\') AS last FROM room_messages WHERE room_id = $1', [roomId]) |
| const last = latestRows[0]?.last |
| const { data, error } = await supabase |
| .from('room_messages') |
| .select('*') |
| .eq('room_id', roomId) |
| .gt('created_at', last) |
| .order('created_at', { ascending: true }) |
| if (error || !data || data.length === 0) return { synced: 0 } |
| await upsertMessagesLocal(data as any[]) |
| return { synced: data.length } |
| } |
|
|
| export async function insertMessage(payload: Database['public']['Tables']['room_messages']['Insert']) { |
| const normalized: Database['public']['Tables']['room_messages']['Insert'] = { |
| ...payload, |
| created_at: payload.created_at || new Date().toISOString() |
| } |
|
|
| if (pool) { |
| const cols = Object.keys(normalized) |
| const vals = Object.values(normalized) |
| const idx = vals.map((_, i) => `$${i + 1}`).join(',') |
| const query = `INSERT INTO room_messages(${cols.join(',')}) VALUES(${idx}) RETURNING *` |
| const res = await pool.query(query, vals) |
| await logEvent({ module: 'messages', operation: 'insert', data: res.rows[0] }) |
| return res.rows[0] |
| } |
|
|
| try { |
| const supabase = await getSupabaseWrite() |
| const { data, error } = await supabase.from('room_messages').insert(normalized).select().single() |
| if (error) { |
| console.error('[persistence.insertMessage] supabase error:', error) |
| if (isDevFallbackAllowed()) { |
| const id = randomUUID() |
| const row = { id, ...normalized } |
| inMemory.room_messages.push(row) |
| await logEvent({ module: 'messages', operation: 'insert_fallback', data: row, error: error.message || null }) |
| return row |
| } |
| return null |
| } |
| const row = data || null |
| await logEvent({ module: 'messages', operation: 'insert', data: row }) |
| return row |
| } catch (e) { |
| console.error('[persistence.insertMessage] supabase error (exception):', e) |
| if (isDevFallbackAllowed()) { |
| const id = randomUUID() |
| const row = { id, ...normalized } |
| inMemory.room_messages.push(row) |
| await logEvent({ module: 'messages', operation: 'insert_fallback', data: row, error: (e as Error)?.message || String(e) }) |
| return row |
| } |
| return null |
| } |
| } |
|
|
| export async function getMessagesByRoom(roomId: string, limit = 50) { |
| |
| if (pool && process.env.SUPABASE_SERVICE_ROLE_KEY) { |
| try { |
| await syncRoomMessagesFromSupabase(roomId) |
| } catch (e) { |
| console.error('[persistence.getMessagesByRoom] sync from supabase failed (ignored):', e) |
| } |
| } |
|
|
| if (pool) { |
| const res = await pool.query('SELECT * FROM room_messages WHERE room_id = $1 ORDER BY created_at ASC LIMIT $2', [roomId, limit]) |
| return res.rows |
| } |
| try { |
| const supabase = await getSupabaseWrite() |
| const { data, error } = await supabase |
| .from('room_messages') |
| .select('*') |
| .eq('room_id', roomId) |
| .order('created_at', { ascending: true }) |
| .limit(limit) |
| if (error) { |
| console.error('[persistence.getMessagesByRoom] supabase error:', error) |
| if (isDevFallbackAllowed()) { |
| return inMemory.room_messages.filter((m) => m.room_id === roomId).slice(0, limit) |
| } |
| return [] |
| } |
| return data || [] |
| } catch (e) { |
| console.error('[persistence.getMessagesByRoom] supabase error (exception):', e) |
| if (isDevFallbackAllowed()) { |
| return inMemory.room_messages.filter((m) => m.room_id === roomId).slice(0, limit) |
| } |
| return [] |
| } |
| } |
|
|