Source: config/migrations/20261003-repair-membera-link-targets.js

/**
 * Migration: `memberA`-Owner-Links von `node`/`tailnet` reparieren.
 *
 * ## Der Bug
 *
 * `createNode` und `createTailnet` schrieben die Owner-Gruppe aus
 * `resolveNodeGroup`/`resolveTailnetGroup` **direkt** in `Links.UIDTarget`. Diese
 * Gruppe kommt aber aus `query(..., { cast: ['UUID'] })` — also als präfixierter
 * **String** `UUID-xxxxxxxx-…` (41 Zeichen, Codebase-Konvention). In die binäre
 * `UIDTarget`-Spalte geschrieben, schneidet MariaDB den String auf 16 Bytes ab:
 *
 *     'UUID-bb73955a-94'  statt  0x11F194B7BB73955AB16F426041107549
 *
 * Der Owner-Link zeigt danach auf eine nicht existierende UID — die Gruppe ist
 * für das Objekt verloren (`isListAdmin`, Gruppenzuordnung im UI).
 * `createProject` macht es richtig (`groupUID = UUID2hex(group.UID)`); der
 * Node-/Tailnet-Pfad wurde analog korrigiert.
 *
 * ## Die Reparatur
 *
 * Das Ziel ist aus `Data.groupUID` (dieselbe präfixierte Form) über
 * `U_UUID2BIN()` rekonstruierbar — die SQL-Entsprechung von `UUID2hex()`.
 * Ein Node ohne eigenes `groupUID` erbt das seines Netzes (`tailnetNode`).
 *
 * Zusätzlich werden **verwaiste** Links entfernt (`UIDTarget` zeigt auf keine
 * ObjectBase-Zeile) — Reste aus früherem Umbenennen/Löschen, die sonst als
 * Geister-Nodes in Listen auftauchen.
 *
 * Idempotent: der `<>`-Vergleich lässt bereits korrekte Links unberührt.
 */

export const id = '20261003-repair-membera-link-targets';
export const name = 'Owner-Links (memberA) von node/tailnet aus Data.groupUID reparieren';

/**
 * @param {{ query: Function }} api
 */
export const migrate = async ({ query }) => {
    // 1. Tailnets: Ziel aus dem eigenen `Data.groupUID`.
    await query(
        `UPDATE Links l
         INNER JOIN ObjectBase o ON (o.UID = l.UID)
         SET l.UIDTarget = U_UUID2BIN(JSON_UNQUOTE(JSON_VALUE(o.Data, '$.groupUID')))
         WHERE l.Type = 'memberA'
           AND o.Type = 'tailnet'
           AND JSON_VALUE(o.Data, '$.groupUID') IS NOT NULL
           AND l.UIDTarget <> U_UUID2BIN(JSON_UNQUOTE(JSON_VALUE(o.Data, '$.groupUID')))`,
        [],
    );

    // 2. Nodes mit eigenem `Data.groupUID` (team/shared).
    await query(
        `UPDATE Links l
         INNER JOIN ObjectBase o ON (o.UID = l.UID)
         SET l.UIDTarget = U_UUID2BIN(JSON_UNQUOTE(JSON_VALUE(o.Data, '$.groupUID')))
         WHERE l.Type = 'memberA'
           AND o.Type = 'node'
           AND JSON_VALUE(o.Data, '$.groupUID') IS NOT NULL
           AND l.UIDTarget <> U_UUID2BIN(JSON_UNQUOTE(JSON_VALUE(o.Data, '$.groupUID')))`,
        [],
    );

    // 3. Nodes ohne eigenes groupUID: Owner-Gruppe des Netzes übernehmen
    //    (`tailnetNode`, UID=Netz, UIDTarget=Node).
    await query(
        `UPDATE Links l
         INNER JOIN ObjectBase o ON (o.UID = l.UID)
         INNER JOIN Links tl ON (tl.UIDTarget = o.UID AND tl.Type = 'tailnetNode')
         INNER JOIN ObjectBase net ON (net.UID = tl.UID AND net.Type = 'tailnet')
         SET l.UIDTarget = U_UUID2BIN(JSON_UNQUOTE(JSON_VALUE(net.Data, '$.groupUID')))
         WHERE l.Type = 'memberA'
           AND o.Type = 'node'
           AND JSON_VALUE(o.Data, '$.groupUID') IS NULL
           AND JSON_VALUE(net.Data, '$.groupUID') IS NOT NULL
           AND l.UIDTarget <> U_UUID2BIN(JSON_UNQUOTE(JSON_VALUE(net.Data, '$.groupUID')))`,
        [],
    );

    // 4. Verwaiste Links der node/tailnet-Objekte entfernen (Ziel existiert
    //    nicht mehr) — Reste aus Umbenennen/Löschen, die sonst als
    //    Geister-Zuordnung in Listen auftauchen.
    await query(
        `DELETE l FROM Links l
         INNER JOIN ObjectBase o ON (o.UID = l.UID)
         WHERE l.Type IN ('memberA', 'tailnetNode')
           AND o.Type IN ('node', 'tailnet')
           AND NOT EXISTS (SELECT 1 FROM ObjectBase t WHERE t.UID = l.UIDTarget)`,
        [],
    );
};