Source: config/migrations/20260924-project-share-owner-backfill.js

// @ts-check
/**
 * AP 30 — Owner-Backfill fuer Bestands-Shares.
 *
 * Neue Shares bekommen ihren Owner beim Anlegen (`addShare`: `member`-Link
 * Share -> Person + `Visible.admin`) — der Anleger ist der Owner
 * ([Shares eines Projekts](https://members.app.commtool.org/-/001-Backend/Datenstruktur/dProjectShares)).
 * Der **Bestand** hat weder Link noch `Visible`-Zeile. Ohne Owner greift die
 * dokumentierte Voreinstellung "Shares erben die Rechte des Projekts" — das ist
 * richtig, laesst aber den Besitz-Anspruch des Anlegers verfallen (er koennte den
 * Share nicht mehr administrieren, sobald er das Projekt-Recht verliert).
 *
 * Dieses Modul zieht den Owner nach. Es ist bewusst **regelbasiert** und enthaelt
 * keine UID: Owner werden die **Personen**-Konten mit `Visible` am Projekt. Das
 * sind echte Nutzer; die Projekt-`admin`-Zeile traegt haeufig ein **Bot-/Systemkonto**
 * (`Type='extern'`), das kein Besitzer eines Shares sein soll.
 *
 * Idempotent (`INSERT IGNORE`), akzeptiert beide Link-Richtungen — die Reihenfolge
 * zum Modul `20260924-project-share-target-model` ist damit gleichgueltig.
 */

export const id = '20260924-project-share-owner-backfill';
export const name = 'Owner-Backfill: Personen mit Projekt-Recht als Share-Owner';

/** Enum-Werte der Share-Objekte in `ObjectBase.Type`. */
const SHARE_TYPES_SQL = `('repositoryShare','directoryShare')`;

/**
 * Zuordnung Share -> Projekt, tolerant gegen **beide** Link-Richtungen
 * (alt: Projekt -> Share, neu: Share -> Projekt).
 */
const SHARE_PROJECT_MAP = `
    SELECT s.UID AS share_uid, p.UID AS project_uid
      FROM ObjectBase AS s
      JOIN Links AS l ON l.UID = s.UID AND l.Type IN ('memberA','member')
      JOIN ObjectBase AS p ON p.UID = l.UIDTarget AND p.Type = 'project'
     WHERE s.Type IN ${SHARE_TYPES_SQL}
    UNION
    SELECT s.UID, p.UID
      FROM ObjectBase AS s
      JOIN Links AS l ON l.UIDTarget = s.UID AND l.Type IN ('memberA','member','projectShare')
      JOIN ObjectBase AS p ON p.UID = l.UID AND p.Type = 'project'
     WHERE s.Type IN ${SHARE_TYPES_SQL}
`;

/**
 * Fuehrt den Owner-Backfill aus.
 *
 * @param {{ query: Function, HEX2uuid: Function }} api
 * @returns {Promise<void>}
 */
export const migrate = async ({ query, HEX2uuid }) => {
    // Kandidaten vor dem Schreiben einsammeln — nur fuer sie aendert sich die
    // Projektion, nicht fuer den ganzen Bestand.
    const affected = await query(
        `SELECT DISTINCT sp.share_uid AS UID, s.Type AS Type
           FROM (${SHARE_PROJECT_MAP}) AS sp
           JOIN ObjectBase AS s ON s.UID = sp.share_uid
           JOIN Visible AS v ON v.UID = sp.project_uid
           JOIN ObjectBase AS owner ON owner.UID = v.UIDUser AND owner.Type = 'person'
          WHERE s.Type IN ${SHARE_TYPES_SQL}`,
        [],
    );

    // ── 1. Owner-Link: Share -> Person (`member`, wie bei der Liste) ──────────
    await query(
        `INSERT IGNORE INTO Links (UID, Type, UIDTarget, UIDuser)
         SELECT DISTINCT sp.share_uid, 'member', v.UIDUser, v.UIDUser
           FROM (${SHARE_PROJECT_MAP}) AS sp
           JOIN ObjectBase AS s ON s.UID = sp.share_uid
           JOIN Visible AS v ON v.UID = sp.project_uid
           JOIN ObjectBase AS owner ON owner.UID = v.UIDUser AND owner.Type = 'person'
          WHERE s.Type IN ${SHARE_TYPES_SQL}`,
        [],
    );

    // ── 2. Besitz-Recht: Owner ist Admin des Shares ───────────────────────────
    // Genau die Zeile, die auch `addShare` fuer neue Shares schreibt.
    await query(
        `INSERT IGNORE INTO Visible (UID, Type, UIDUser)
         SELECT DISTINCT sp.share_uid, 'admin', v.UIDUser
           FROM (${SHARE_PROJECT_MAP}) AS sp
           JOIN ObjectBase AS s ON s.UID = sp.share_uid
           JOIN Visible AS v ON v.UID = sp.project_uid
           JOIN ObjectBase AS owner ON owner.UID = v.UIDUser AND owner.Type = 'person'
          WHERE s.Type IN ${SHARE_TYPES_SQL}`,
        [],
    );

    // ── 3. Projektion nachziehen ──────────────────────────────────────────────
    // Der Owner aendert `share_grants` (die IDE-Rechte der Person am Repo).
    // Ohne Event bliebe die Projektion auf dem Stand ohne Owner.
    const { writeEventLog, buildEventPayload } = await import('../../utils/events.js');
    const { getOrganizationForShare } = await import('../../utils/organizationUtils.js');

    const seen = new Set();
    for (const share of affected) {
        const uid = HEX2uuid(share.UID);
        if (!uid || seen.has(uid)) continue;
        seen.add(uid);
        const organization = await getOrganizationForShare(share.UID);
        await writeEventLog(`/change/${share.Type}/${uid}`, buildEventPayload({ organization, data: [uid] }));
    }
};