export const id = '20260928-app-registry-types';
export const name = 'App-Registry: app/appDomain/appAsset in ObjectBase.Type und Links.Type';
/**
* Das App-Registry legt Apps, Domains und Assets als Objekte in `ObjectBase`
* ab und verbindet sie über `Links`. Beide Tabellen sind `WITH SYSTEM
* VERSIONING` und führen `Type` als `enum` — **ohne die Literale schlägt jeder
* INSERT fehl** (strikt in MariaDB, kein stiller Fallback).
*
* ## Warum die Werte gelesen und nicht hingeschrieben werden
*
* Die naheliegende Variante wäre, die vollständige Enum-Liste in die Migration
* zu schreiben (so machen es die Accounting- und Runner-Migrationen). Das ist
* hier bewusst **nicht** gewählt: die Ist-Enum hängt davon ab, welche
* Migrationskette eine Datenbank durchlaufen hat — gemessen unterscheiden sich
* Test- und Produktionsstand bereits sichtbar (`runner` fehlt dem einen,
* `project` dem anderen). Eine hart kodierte Liste würde beim Anwenden auf einen
* abweichenden Stand **still Literale entfernen** und damit bestehende Zeilen
* unlesbar machen.
*
* Diese Migration liest deshalb die tatsächliche Enum-Definition aus
* `information_schema` und **hängt nur die fehlenden Werte an**. Sie ist damit
* idempotent und auf jedem Startzustand korrekt — auch auf einem frischen Boot,
* der `initTables.sql` schon kennt (dort endet sie als No-Op).
*
* ## Das Session-Flag ist Pflicht
*
* MariaDB verweigert `ALTER TABLE` auf system-versioned Tabellen mit
* `ERROR 4119` („Change @@system_versioning_alter_history to proceed"). Das Flag
* gilt **pro Verbindung** — hier ist das unkritisch, weil `query()` ohne
* Transaktion auf der geteilten `mainConnection` läuft. Würde die Migration auf
* einen Pool umgestellt, müssten SET und ALTER auf dieselbe Verbindung.
*/
/** Objekttypen für das App-Registry (ObjectBase.Type) */
const OBJECT_TYPES = ['app', 'appDomain', 'appAsset'];
/**
* Link-Typen für das App-Registry (Links.Type).
* `app` selbst ist kein Link-Typ — eine App wird nicht verlinkt, sie ist das
* Ziel. Domains und Assets hängen über Links an der App.
*/
const LINK_TYPES = ['appDomain', 'appAsset'];
/**
* Liest die Enum-Literale einer Spalte aus `information_schema`.
* @param {Function} query
* @param {string} table
* @param {string} column
* @returns {Promise<string[]|null>} Literale, oder `null` wenn keine enum-Spalte
*/
const getEnumValues = async (query, table, column) => {
const rows = await query(
`SELECT COLUMN_TYPE AS columnType
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND COLUMN_NAME = ?`,
[table, column],
);
const columnType = rows[0]?.columnType;
if (!columnType) return null;
const match = /^enum\((.*)\)$/is.exec(columnType);
if (!match) return null;
// Über die Anführungszeichen parsen statt über Kommas zu splitten — ein
// Literal darf theoretisch ein Komma enthalten, ein Quote-Token nicht.
const literals = match[1].match(/'((?:[^']|'')*)'/g);
if (!literals) return null;
return literals.map((token) => token.slice(1, -1).replace(/''/g, "'"));
};
/**
* Hängt fehlende Literale an ein `enum` an — in der Reihenfolge, in der sie
* übergeben werden. Vorhandene Werte behalten ihre Position, damit bestehende
* Zeilen ihren Index nicht verlieren.
* @param {string[]} current
* @param {string[]} wanted
* @returns {string[]|null} neue Liste, oder `null` wenn nichts fehlt
*/
const appendMissing = (current, wanted) => {
const missing = wanted.filter((value) => !current.includes(value));
return missing.length > 0 ? [...current, ...missing] : null;
};
/**
* Führt ein `ALTER TABLE … MODIFY COLUMN Type enum(…)` auf einer
* system-versioned Tabelle aus. Setzt und entfernt das Session-Flag selbst,
* damit kein Aufrufer es vergessen kann.
* @param {Function} query
* @param {string} table
* @param {string[]} values
*/
const alterEnum = async (query, table, values) => {
const list = values.map((value) => `'${value.replace(/'/g, "''")}'`).join(',');
await query('SET @@system_versioning_alter_history = 1', []);
try {
await query(
`ALTER TABLE \`${table}\`
MODIFY COLUMN \`Type\` enum(${list})
CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci NOT NULL`,
[],
);
} finally {
// Auch im Fehlerfall zurücksetzen — sonst bliebe das Flag für alle
// folgenden Statements dieser Verbindung gesetzt (mainConnection).
await query('SET @@system_versioning_alter_history = 0', []);
}
};
export const migrate = async ({ query }) => {
const targets = [
{ table: 'ObjectBase', wanted: OBJECT_TYPES },
{ table: 'Links', wanted: LINK_TYPES },
];
for (const { table, wanted } of targets) {
const current = await getEnumValues(query, table, 'Type');
if (!current) {
// Kein enum gefunden: Tabelle fehlt oder Spalte ist anders typisiert.
// Nicht raten — die Migration würde sonst gegen ein unbekanntes
// Schema schreiben. initTables.sql-Pfad ist hier bewusst kein Fehler.
continue;
}
const next = appendMissing(current, wanted);
if (!next) continue;
await alterEnum(query, table, next);
}
};