Source: config/migrations/20260925-bot-repo-link.js

export const id = '20260925-bot-repo-link';
export const name = 'Add botRepo to Links.Type enum';

/**
 * Ergänzt `botRepo` in `Links.Type`.
 *
 * Ein „Bot-Repo" bündelt die Bots eines Repositories (z. B. alle Bots von
 * `basic-bots`) und ist bewusst **nicht** organisationsgebunden: die UID wird im
 * Repo selbst vergeben (`botRepo.json`) und existiert nur als Link-Endpunkt —
 * genau wie die Bot-UIDs, die ebenfalls keine `ObjectBase`-Zeilen sind.
 *
 * `Links.Type='botRepo'`: UID = Repo-UID, UIDTarget = Bot-UID.
 *
 * Bewusst nur eine Migration auf `Links.Type`: `ObjectBase` bleibt unberührt,
 * es wird **kein** neuer Objekt-Typ gebraucht. Eine `Links`-Zeile trägt keine
 * Datenspalte, es wird also nichts „am Repo" gespeichert — die Beziehung ist
 * der ganze Inhalt. Sollte später doch Repo-Metadaten (Name, Icon, …) nötig
 * werden, ist der Weg eine Companion-Tabelle analog zu `RunnerData`.
 *
 * Bewusst **nicht** `appBot`/`app` genannt: `app`, `appDomain` und `appAsset`
 * sind im App-Registry-Konzept (`PLAN-app-registry.md`) für org-gebundene,
 * gehostete Frontends reserviert (Domain, Branding, PWA-Icons) — hier ist
 * dagegen ein globales Bot-Repo gemeint.
 *
 * ## Warum die vorhandenen Werte gelesen werden
 *
 * Die Enum-Listen sind je Umgebung auseinandergelaufen und werden von der
 * Migrationskette aufgebaut (`initTables.sql` ist ein alter Stand, jede
 * Link-Typ-Migration ergänzt). Prod hat die 2026-09 ausgemusterte
 * Runner-Runtime bereits hinter sich, Dev nicht:
 *
 * - Dev (`member`) führt zusätzlich `deploymentRunner`, `workspaceRunner` und
 *   `workspaceLease` — und zwar **in der History**. `Links` ist
 *   system-versioniert und nach Zeit partitioniert; `system_versioning_alter_history
 *   = 1` prüft das ALTER gegen diese Partitionen. Eine Liste ohne diese Werte
 *   schneidet dort ab und die Migration bricht mit „Data truncated for column
 *   'Type'" ab (Zeile 295490).
 * - Prod führt sie nicht und soll sie auch nicht bekommen: `20260827-tailnet-object-types`
 *   hat sie dort bewusst entfernt.
 *
 * Deshalb wird `botRepo` an die **jeweils vorhandene** Liste angehängt, statt
 * eine kanonische Liste zu schreiben. Eine feste Liste wäre in beide Richtungen
 * falsch: zu kurz für Dev (Abbruch), zu lang für Prod (rüstet ausgemusterte
 * Typen wieder ein und macht die Entfernung aus `20260827` rückgängig).
 *
 * Zeichensatz und Collation kommen ebenfalls aus der Spalte, damit die
 * Migration dort nichts umstellt.
 */
export const migrate = async ({ query }) => {
    const [column] = await query(`
        SELECT COLUMN_TYPE AS columnType,
               CHARACTER_SET_NAME AS charset,
               COLLATION_NAME AS collation
        FROM information_schema.COLUMNS
        WHERE TABLE_SCHEMA = DATABASE()
          AND TABLE_NAME = 'Links'
          AND COLUMN_NAME = 'Type'
    `, []);

    const existing = parseEnumValues(column?.columnType);
    if (existing.length === 0) {
        throw new Error('Links.Type: Enum-Werte konnten nicht gelesen werden');
    }

    // Wiederholbar: ein erneuter Lauf soll nichts ändern und kein ALTER auslösen.
    if (existing.includes('botRepo')) {
        return;
    }

    const enumList = [...existing, 'botRepo']
        .map(value => `'${value.replace(/'/g, "''")}'`)
        .join(',');

    // Zeichensatz/Collation aus der Spalte übernehmen; fehlen sie (kein
    // String-Typ), bleibt die Spaltenvorgabe unangetastet.
    const charset = column.charset ? ` CHARACTER SET ${column.charset}` : '';
    const collation = column.collation ? ` COLLATE ${column.collation}` : '';

    // `system_versioning_alter_history = 1` erlaubt das Ändern einer
    // system-versionierten Tabelle; ohne das schlägt das ALTER fehl. Der Wert
    // wird in `finally` zurückgesetzt, damit eine fehlgeschlagene Migration
    // keine offene Session-Einstellung hinterlässt.
    await query('SET @@system_versioning_alter_history = 1', []);
    try {
        await query(`
            ALTER TABLE Links
            MODIFY COLUMN \`Type\` enum(${enumList})${charset}${collation} NOT NULL
        `, []);
    } finally {
        await query('SET @@system_versioning_alter_history = 0', []);
    }
};

/**
 * Liest die Werte einer `enum(...)`-Spaltendefinition aus.
 *
 * Die Werte dürfen Kommas und Klammern enthalten, deshalb wird zeichenweise
 * gelesen statt an Kommas zu trennen. MySQL/MariaDB verdoppelt Anführungszeichen
 * innerhalb eines Werts (`'a''b'`).
 *
 * @param {string|undefined} columnType - z. B. `enum('member','action')`
 * @returns {string[]} Werte in Deklarationsreihenfolge; leer, wenn kein Enum
 *
 * @example
 * parseEnumValues("enum('a','b')") // ['a', 'b']
 */
export const parseEnumValues = (columnType) => {
    if (typeof columnType !== 'string') return [];
    const open = columnType.indexOf('(');
    const close = columnType.lastIndexOf(')');
    if (open === -1 || close <= open) return [];

    const body = columnType.slice(open + 1, close);
    const values = [];
    let current = null;

    for (let i = 0; i < body.length; i++) {
        const char = body[i];

        if (current === null) {
            if (char === "'") current = '';
            continue;
        }

        if (char === "'") {
            if (body[i + 1] === "'") {
                current += "'";
                i++;
                continue;
            }
            values.push(current);
            current = null;
            continue;
        }

        current += char;
    }

    return values;
};