Blog
Architecture
12 ottobre 20258 min

Gruppi gerarchici a 6 livelli: la funzionalità più complessa

Punti chiave: Costruire un sistema di gruppi gerarchici a 6 livelli di profondità con 4 ruoli, query ricorsive PostgreSQL (CTE) e 31 politiche RLS, è la funzionalità più complessa di TAMSIV. Questo articolo copre il modello di dati, l'ereditarietà dei permessi, il componente frontend HierarchicalGroupPicker e le lezioni apprese.

Ci sono funzionalità che sembrano semplici sulla carta. "Aggiungi gruppi" — sembra innocente. Come aggiungere un carrello in un'app di e-commerce. Poi inizi a scavare e ti rendi conto di aver appena aperto il vaso di Pandora.

TAMSIV supporta gruppi gerarchici fino a 6 livelli di profondità. Il mio club di immersioni è stato il caso d'uso perfetto: Club → Commissione Tecnica → Livello 1 → Gruppo del martedì. Una famiglia usa: Famiglia → Casa → Cucina / Giardino / Garage. Una PMI: Azienda → Dipartimento → Team → Progetto.

Questa è la funzionalità che mi ha richiesto più tempo. È anche quella che rende TAMSIV utilizzabile per organizzazioni reali, non solo per uso individuale.

Riunione di squadra attorno a un moderno tavolo da conferenza con laptop, illuminazione calda
I gruppi collaborativi trasformano un'app personale in uno strumento di squadra.

Come modellare gruppi gerarchici in PostgreSQL?

Il modello di dati si basa su un concetto semplice: ogni gruppo ha un parent_id che punta al suo gruppo genitore. Un gruppo radice ha parent_id = NULL.

-- Tabella semplificata
CREATE TABLE collaborative.groups (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  parent_id UUID REFERENCES collaborative.groups(id),
  created_by UUID REFERENCES auth.users(id),
  depth INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT now()
);

Per visualizzare l'albero completo di un gruppo, utilizzo query ricorsive (Common Table Expressions, o CTE) in PostgreSQL:

WITH RECURSIVE group_tree AS (
  -- Base: il gruppo radice
  SELECT id, name, parent_id, 0 AS depth
  FROM collaborative.groups
  WHERE id = p_group_id

  UNION ALL

  -- Ricorsione: i sottogruppi
  SELECT g.id, g.name, g.parent_id, gt.depth + 1
  FROM collaborative.groups g
  JOIN group_tree gt ON g.parent_id = gt.id
  WHERE gt.depth < 6  -- Salvaguardia
)
SELECT * FROM group_tree;

La clausola WHERE depth < 6 è una salvaguardia critica. Senza di essa, un errore nei dati (un ciclo di riferimenti, ad esempio) farebbe eseguire la query in un loop infinito. Sei livelli coprono tutti i casi d'uso reali che ho incontrato.

Questo modello di dati risiede nello schema collaborative, separato dal resto — spiego perché nell'articolo sulla strutturazione del database.

Quali sono i quattro ruoli e come funzionano?

Ogni membro di un gruppo ha un ruolo che determina i suoi permessi:

  • Admin — Fare tutto: creare, modificare, eliminare, gestire i membri, invitare, modificare le impostazioni del gruppo. È il creatore del gruppo per impostazione predefinita.
  • Manager — Gestire il contenuto: creare/modificare/eliminare attività e memo, assegnare membri, convalidare le checklist. Ma non gestire i membri o le impostazioni.
  • Member — Contribuire: creare contenuto, vedere tutto il contenuto del gruppo, convalidare i propri elementi della checklist.
  • Viewer — Sola lettura: vedere il contenuto, nient'altro. Utile per osservatori o stakeholder che vogliono seguire l'avanzamento senza intervenire.

La trappola dell'ereditarietà dei permessi

Se sei Admin del gruppo "Club di immersioni", sei automaticamente Admin di "Commissione Tecnica" (un sottogruppo)? In TAMSIV, la risposta è — ma con delle sfumature.

L'ereditarietà funziona verso il basso: un Admin di un gruppo genitore ha gli stessi diritti in tutti i sottogruppi. Ma un Admin di un sottogruppo non ha alcun diritto nel gruppo genitore. Questa asimmetria è naturale (il direttore vede tutto, il capo squadra vede la sua squadra), ma aggiunge complessità alle query SQL.

Per verificare i permessi, ogni query deve risalire l'albero gerarchico fino a trovare un ruolo o raggiungere la radice:

-- L'utente ha un ruolo in questo gruppo o in un genitore?
WITH RECURSIVE parent_chain AS (
  SELECT id, parent_id FROM collaborative.groups WHERE id = p_group_id
  UNION ALL
  SELECT g.id, g.parent_id
  FROM collaborative.groups g
  JOIN parent_chain pc ON g.id = pc.parent_id
)
SELECT role FROM collaborative.group_members
WHERE group_id IN (SELECT id FROM parent_chain)
AND user_id = auth.uid()
ORDER BY role ASC  -- Admin > Manager > Member > Viewer
LIMIT 1;
Organigramma appuntato su una bacheca di sughero con puntine colorate e fili di collegamento
Ogni livello della gerarchia ha le proprie regole di accesso.

Come scrivere e testare 31 politiche RLS?

Questo è il numero che fa male: 31 politiche Row Level Security per lo schema collaborative. Supabase utilizza RLS per proteggere l'accesso ai dati — ogni tabella ha regole che determinano chi può leggere, scrivere, modificare, eliminare.

Ecco alcuni esempi concreti:

Politica di lettura semplice

-- Un membro può vedere le attività dei suoi gruppi
CREATE POLICY "members_read_tasks" ON collaborative.group_tasks
  FOR SELECT USING (
    EXISTS (
      SELECT 1 FROM collaborative.group_members gm
      WHERE gm.group_id = group_tasks.group_id
      AND gm.user_id = auth.uid()
    )
  );

Politica con ereditarietà gerarchica

-- Un admin può eliminare le attività dei suoi gruppi E sottogruppi
CREATE POLICY "admin_delete_tasks" ON collaborative.group_tasks
  FOR DELETE USING (
    EXISTS (
      WITH RECURSIVE parent_chain AS (
        SELECT id, parent_id FROM collaborative.groups
        WHERE id = group_tasks.group_id
        UNION ALL
        SELECT g.id, g.parent_id
        FROM collaborative.groups g
        JOIN parent_chain pc ON g.id = pc.parent_id
      )
      SELECT 1 FROM collaborative.group_members gm
      WHERE gm.group_id IN (SELECT id FROM parent_chain)
      AND gm.user_id = auth.uid()
      AND gm.role = 'admin'
    )
  );

Testare 31 politiche significa almeno 31 scenari di test. In realtà, è molto di più, perché ogni politica deve essere testata positivamente (l'accesso è concesso) E negativamente (l'accesso è negato). Metodico, noioso, indispensabile. I dettagli sull'approccio di test sono nell'articolo sull'audit di sicurezza.

Come costruire il componente HierarchicalGroupPicker?

Lato frontend, visualizzare un albero di gruppi con indentazione, icone di ruolo e interazioni, è una sfida UI a sé stante.

Il HierarchicalGroupPicker è un componente React Native che:

  • Visualizza l'albero con indentazione proporzionale alla profondità (utilizzando spacing() del sistema di scaling).
  • Indica il ruolo dell'utente in ogni gruppo tramite un'icona colorata.
  • Permette la selezione con un toggle "includi sottogruppi" — quando selezioni un gruppo genitore, i sottogruppi possono essere inclusi automaticamente.
  • Supporta il fold/unfold: i sottogruppi sono ripiegabili per non ingombrare lo schermo su gerarchie profonde.

Questo componente è riutilizzato ovunque: nel filtro dell'agenda, nella creazione di attività, nella schermata di gestione dei gruppi. È un enorme investimento di tempo all'inizio, ma un guadagno di tempo colossale in seguito.

Il pattern FilterBar

Il HierarchicalGroupPicker funziona con la FilterBar, un componente di filtraggio con 3 contesti (Privato / Condiviso / Tutto) e modalità (tutte le attività / create da me / assegnate a me). La combinazione dei due permette query molto precise: "mostrami le attività assegnate a me nel gruppo Famiglia e nei suoi sottogruppi".

Bambole russe matrioska allineate dalla più grande alla più piccola su uno scaffale di legno
Gruppi in gruppi in gruppi — come le bambole russe.

Quali sono i casi d'uso reali dei gruppi gerarchici?

Ecco i casi d'uso che i miei tester utilizzano attivamente:

  • Famiglia: Famiglia → Casa (attività domestiche) / Vacanze (pianificazione) / Scuola (compiti dei bambini). Ne parlo nell'articolo sull'organizzazione familiare.
  • Club sportivo: Club → Commissioni → Livelli → Gruppi per fascia oraria.
  • Piccolo team: Azienda → Dipartimento → Progetto → Sprint.
  • Associazione: Associazione → Polo → Evento.

Il limite di 6 livelli è raramente raggiunto. In pratica, 3-4 livelli coprono il 95% delle esigenze. Ma questa profondità massima è una rete di sicurezza — meglio averla e non averne bisogno.

Quali lezioni trarre da questa implementazione?

  1. Le query ricorsive PostgreSQL sono potenti ma costose: Ogni query che risale la gerarchia esegue N join (dove N = la profondità). Un indice su parent_id è indispensabile.
  2. Le politiche RLS si accumulano rapidamente: Ogni tabella × ogni operazione (SELECT, INSERT, UPDATE, DELETE) × ogni ruolo = molte politiche. Documentale in un file dedicato.
  3. L'ereditarietà dei permessi è il punto più delicato: Decidi presto se i permessi si propagano verso il basso, verso l'alto o entrambi. Cambiare questa decisione in seguito è estremamente costoso.
  4. Il componente frontend è un investimento: Un buon HierarchicalGroupPicker richiede una settimana per essere costruito correttamente, ma si riutilizza ovunque.
  5. Testa con dati reali: Un albero di 3 gruppi in sviluppo non rivela gli stessi bug di un albero di 15 gruppi su 4 livelli in produzione.

Se costruisci un sistema collaborativo, la gerarchia è ciò che trasforma una semplice "lista di gruppi" in uno strumento che le organizzazioni adottano realmente. È difficile, è lungo, ma è ciò che fa la differenza con i concorrenti che si limitano a gruppi piatti.

FAQ

Perché limitare a 6 livelli di profondità?

Sei livelli coprono tutti i casi d'uso reali che ho incontrato. Oltre, l'albero diventa ingestibile per l'utente. Il limite protegge anche da query ricorsive troppo profonde che potrebbero influire sulle prestazioni. In pratica, 3-4 livelli sono sufficienti per il 95% delle organizzazioni.

Le query ricorsive causano problemi di performance?

Con un indice su parent_id e un limite di profondità, le prestazioni sono eccellenti anche con centinaia di gruppi. Una query ricorsiva su 6 livelli impiega meno di 10ms su Supabase PostgreSQL. Il vero rischio è un ciclo di riferimenti (gruppo A genitore di B, B genitore di A) — la clausola WHERE depth < 6 protegge da questo.

Come gestire l'invito di nuovi membri?

L'admin di un gruppo può invitare via email o tramite link di condivisione. L'invitato si unisce al gruppo con il ruolo scelto dall'admin. Se il gruppo ha sottogruppi, l'invitato ha accesso solo al gruppo in cui è stato invitato — a meno che un admin del genitore non gli dia esplicitamente accesso.

Le checklist di gruppo funzionano diversamente dalle checklist personali?

Sì. Le checklist di gruppo hanno due modalità di validazione: "single" (una sola persona valida per tutti) e "everyone" (ogni membro deve validare individualmente). La modalità "everyone" è perfetta per le liste della spesa familiari dove nessuno compra la stessa cosa.

È possibile spostare un gruppo in un altro gruppo genitore?

Sì, un admin può riorganizzare la gerarchia cambiando il parent_id di un gruppo. Ma è un'operazione delicata: i permessi ereditati cambiano immediatamente, e i membri del nuovo genitore vedono il contenuto del gruppo spostato. Un messaggio di conferma esplicito avverte l'admin delle conseguenze.