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;
};