Source: config/migrations/20260924-project-share-target-model.js

// @ts-check
/**
 * AP 30 — Projekt-/Share-Zielmodell.
 *
 * Alt (Ist):
 *  - `ObjectBase.UIDBelongsTo` trug die **Organisation** (Org-Gruppe)
 *  - der Projekt-Link zeigte vom Projekt zum Share
 *    (`Links.UID` = Projekt, `UIDTarget` = Share, Type `memberA`/`member`/`projectShare`)
 *  - die Lesestufe steckte im Link-Typ (`memberA` = schreiben, `member` = lesen)
 *
 * Neu (Ziel, normativ seit 2026-09-24):
 *  - `UIDBelongsTo` = eigene UID. Es transportiert **keine** Organisation mehr;
 *    die Org wird ueber die `memberA`-Kette abgeleitet
 *    (`getOrganizationForObject`, siehe 080-Workspaces/051-Migrationsplan-Komponenten).
 *    Ein Ableger zeigt auf seinen Basis-Share (bleibt unveraendert).
 *  - der Projekt-Link zeigt vom Share zum Projekt (`UID` = Share, `UIDTarget` = Projekt)
 *  - die Lesestufe ist eine Eigenschaft des Shares: `metadata.mode`
 *    (`readOnly` sperrt, alles andere ist editierbar)
 *
 * Die Migration ist idempotent und laeuft ueber den normalen Migrationslauf
 * (`dbVersion`). Sie aendert ausschliesslich Basisobjekte: ein Share/Projekt,
 * dessen Elternobjekt selbst ein Share/Projekt ist, bleibt ein Ableger.
 */

export const id = '20260924-project-share-target-model';
export const name = 'Projekt/Share: Selbstreferenz und Share->Projekt-Link';

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

/**
 * Fuehrt die Datenmigration aus.
 *
 * @param {{ query: Function, UUID2hex: Function, HEX2uuid: Function }} api
 * @returns {Promise<void>}
 */
export const migrate = async ({ query, HEX2uuid }) => {
    // Kandidaten **vor** dem Umbau einsammeln: nur fuer diese Objekte aendert sich
    // die Projektion (`organization_id` / `root_uid`), nicht fuer den ganzen Bestand.
    const affectedShares = await query(
        `SELECT s.UID, s.Type FROM ObjectBase s
           JOIN ObjectBase o ON o.UID = s.UIDBelongsTo
          WHERE s.Type IN ${SHARE_TYPES_SQL} AND o.Type = 'group'`,
        [],
    );
    const affectedProjects = await query(
        `SELECT p.UID FROM ObjectBase p
           JOIN ObjectBase o ON o.UID = p.UIDBelongsTo
          WHERE p.Type = 'project' AND o.Type = 'group'`,
        [],
    );

    // ── 1. Lesestufe wandert vom Link auf das Objekt ────────────────────────
    // Ein `member`-Link hiess "nur lesen". Im Zielmodell ist das
    // `metadata.mode = 'readOnly'` am Share. Vorhandene Modi bleiben unberuehrt.
    await query(
        `UPDATE ObjectBase s
           JOIN Links l ON l.UIDTarget = s.UID AND l.Type = 'member'
           JOIN ObjectBase p ON p.UID = l.UID AND p.Type = 'project'
            SET s.Data = JSON_SET(s.Data, '$.metadata.mode', 'readOnly')
          WHERE s.Type IN ${SHARE_TYPES_SQL}
            AND JSON_VALID(s.Data)
            AND JSON_VALUE(s.Data, '$.metadata.mode') IS NULL`,
        [],
    );

    // ── 2. Projekt-Link umdrehen: Projekt -> Share  ==>  Share -> Projekt ───
    // Erst die neue Richtung anlegen (idempotent), dann die alte entfernen.
    await query(
        `INSERT IGNORE INTO Links (UID, Type, UIDTarget, UIDuser)
         SELECT l.UIDTarget, 'memberA', l.UID, l.UIDuser
           FROM Links l
           JOIN ObjectBase s ON s.UID = l.UIDTarget AND s.Type IN ${SHARE_TYPES_SQL}
           JOIN ObjectBase p ON p.UID = l.UID AND p.Type = 'project'
          WHERE l.Type IN ('memberA','member','projectShare')`,
        [],
    );
    await query(
        `DELETE l FROM Links l
           JOIN ObjectBase s ON s.UID = l.UIDTarget AND s.Type IN ${SHARE_TYPES_SQL}
           JOIN ObjectBase p ON p.UID = l.UID AND p.Type = 'project'
          WHERE l.Type IN ('memberA','member','projectShare')`,
        [],
    );

    // ── 3. Organisation aus dem Objekt entfernen ────────────────────────────
    // Basisobjekte zeigen auf sich selbst. Ableger (Eltern = anderes
    // Projekt/Share) behalten ihren Zeiger auf den Basis-Share.
    await query(
        `UPDATE ObjectBase s
           JOIN ObjectBase o ON o.UID = s.UIDBelongsTo
            SET s.UIDBelongsTo = s.UID
          WHERE s.Type IN ${SHARE_TYPES_SQL} AND o.Type = 'group'`,
        [],
    );
    await query(
        `UPDATE ObjectBase p
           JOIN ObjectBase o ON o.UID = p.UIDBelongsTo
            SET p.UIDBelongsTo = p.UID
          WHERE p.Type = 'project' AND o.Type = 'group'`,
        [],
    );

    // ── 4. Projektion nachziehen ────────────────────────────────────────────
    // `rag-sync` (rightsSync) projiziert `organization_id` und `root_uid` und
    // liest dafuer den `eventLog`-Cursor. Ohne diese Events blieben die
    // projizierten Werte auf dem alten Stand (root_uid = Org-Gruppe).
    // Nur die tatsaechlich betroffenen Objekte werden gemeldet.
    const { writeEventLog, buildEventPayload } = await import('../../utils/events.js');
    const { getOrganizationForObject, getOrganizationForShare } = await import('../../utils/organizationUtils.js');

    for (const project of affectedProjects) {
        const uid = HEX2uuid(project.UID);
        const organization = await getOrganizationForObject(project.UID);
        await writeEventLog(`/change/project/${uid}`, buildEventPayload({ organization, data: [uid] }));
    }
    for (const share of affectedShares) {
        const uid = HEX2uuid(share.UID);
        const organization = await getOrganizationForShare(share.UID);
        await writeEventLog(`/change/${share.Type}/${uid}`, buildEventPayload({ organization, data: [uid] }));
    }
};