// @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] }));
}
};