/* |-------------------------------------------------------------------------- | Agência PSI — Edge Functions: helper schema-per-tenant |-------------------------------------------------------------------------- | As tabelas tenant-scoped vivem em schemas físicos `tenant_` SEM a | coluna tenant_id (docs/F0_categorizacao.md). Edge functions resolvem o | schema a partir do tenant_id (que já chega via URL/body/linha) e usam | `tdb.from(...)` para tabelas tenant. Tabelas GLOBAIS (tenants, | tenant_members, profiles, subscriptions, addon_*, whatsapp_credit_*, | channel_routing, audit_logs...) e RPCs continuam via o client público. | | Como edge functions usam service_role, `.schema(x)` exige que o schema | esteja exposto no PostgREST (config.toml, F5). Schemas tenant entram lá | na criação do tenant. |-------------------------------------------------------------------------- */ import { createClient, type SupabaseClient } from 'https://esm.sh/@supabase/supabase-js@2' /** Espelha public.tenant_schema_name(slug). */ export function tenantSchemaName(slug: string | null | undefined): string | null { if (typeof slug !== 'string') return null if (!/^[a-z][a-z0-9_]{1,47}$/.test(slug)) return null return `tenant_${slug}` } /** Client service_role no schema public (tabelas globais + RPCs). */ export function adminClient(): SupabaseClient { return createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!, ) } /** tenant_id -> nome do schema (via tenants.slug). null se não existir. */ export async function schemaForTenant(admin: SupabaseClient, tenantId: string): Promise { if (!tenantId) return null const { data, error } = await admin.from('tenants').select('slug').eq('id', tenantId).maybeSingle() if (error) { console.error('[tenant] schemaForTenant erro:', error.message) return null } return tenantSchemaName(data?.slug ?? null) } /** * tenant_id -> client ligado ao schema do tenant (para tabelas tenant). * Lança se o tenant não existir/sem slug — chamar tabela tenant sem schema é bug. */ export async function tenantDbForId(admin: SupabaseClient, tenantId: string): Promise { const schema = await schemaForTenant(admin, tenantId) if (!schema) throw new Error(`[tenant] schema indisponível para tenant ${tenantId}`) return admin.schema(schema) } export type TenantRef = { tenantId: string; slug: string; schema: string } /** Lista tenants ativos com schema provisionado — base dos crons que varrem todos. */ export async function listTenantSchemas(admin: SupabaseClient): Promise { // tenant_schemas é populada por clone_tenant_template (F1/F2); join garante slug atual const { data, error } = await admin .from('tenant_schemas') .select('tenant_id, schema_name, tenants!inner(slug)') if (error) { console.error('[tenant] listTenantSchemas erro:', error.message) return [] } return (data ?? []) .map((r: Record) => { const slug = (r.tenants as { slug?: string } | null)?.slug ?? null const schema = tenantSchemaName(slug) return schema ? { tenantId: r.tenant_id as string, slug: slug as string, schema } : null }) .filter((x): x is TenantRef => x !== null) } /** * Roteia um webhook inbound -> tenant, via public.channel_routing. * Usado quando a function NÃO recebe tenant_id na URL (ex.: Meta Cloud API, * que identifica o canal por phone_number_id). Os webhooks Twilio/Evolution * deste projeto recebem tenant_id na própria URL e NÃO precisam disto. */ export async function resolveTenantByChannel( admin: SupabaseClient, keys: { senderAddress?: string | null; twilioPhone?: string | null; twilioSid?: string | null }, ): Promise<(TenantRef & { channelId: string }) | null> { let q = admin.from('channel_routing').select('channel_id, tenant_id, tenants!inner(slug)').limit(1) if (keys.twilioSid) q = q.eq('twilio_subaccount_sid', keys.twilioSid) else if (keys.twilioPhone) q = q.eq('twilio_phone_number', keys.twilioPhone) else if (keys.senderAddress) q = q.eq('sender_address', keys.senderAddress) else return null const { data, error } = await q.maybeSingle() if (error || !data) { if (error) console.error('[tenant] resolveTenantByChannel erro:', error.message) return null } const slug = (data.tenants as { slug?: string } | null)?.slug ?? null const schema = tenantSchemaName(slug) if (!schema) return null return { tenantId: data.tenant_id as string, slug: slug as string, schema, channelId: data.channel_id as string } }