Source: config/migrations/20261003-app-backend-branches.js

export const id = '20261003-app-backend-branches';
export const name = 'Backend-Zweige und Org-Zuordnung: AppBackendBranch und OrgAppDeployment';

/**
 * Backends werden ein **eigener, benannter Begriff** — und eine Admin-Wahl.
 *
 * ## Der Befund, der dahin führt
 *
 * `AppRelease.Backends` (Migration `20260928-app-release`) hatte **keinen
 * Schreiber**: `saveAppRelease`, `setCurrentRelease` und `setOrgReleaseOverride`
 * waren repoweit ohne Aufrufer, und der `registryRouter` hatte ausschließlich
 * `GET`-Routen. Die Frage „wo legt der Orga-Admin fest, zu welchem Backend die
 * App routet?" hatte damit **keine** Antwort — nicht eine schlechte.
 *
 * Dazu kam die Form: `Backends` lag **in** der Release-Zeile, also pro
 * `(AppKey, Version)`. Ein Backend-Wechsel bei gleichem Frontend brauchte damit
 * eine eigene Zeile mit unverändertem `Prefix` — formal korrekt, praktisch eine
 * Vervielfachung der Release-Zeilen für eine Einstellung, die sich unabhängig
 * ändert.
 *
 * ## Die Trennung: Angebot / Wahl / Frontend
 *
 * Drei Tabellen, drei Zuständigkeiten — und **keine** Überschneidung:
 *
 * | Tabelle | Frage | Autoritativ |
 * |---|---|---|
 * | `AppBackendBranch` | welche Zweige sind **wählbar**? | die Vertrags-Datei |
 * | `AppRelease` | welches Frontend-**Artefakt** gibt es? | der Tag / CI |
 * | `OrgAppDeployment` | was hat **diese** Org gewählt? | der Org-Admin |
 *
 * Der Vertrag (YAML, importiert) fasst `OrgAppDeployment` **nie** an. Ein
 * Re-Import kann damit eine Entscheidung nicht zurücksetzen — es gibt keinen
 * gemeinsamen Schlüssel, über den beide schreiben. Genau das macht die Ablage
 * der Datei beliebig: sie kann im Repo liegen, gemountet sein, später aus der
 * CommTool-Release-App kommen.
 *
 * ## Warum `Version` in `OrgAppDeployment` `NULL`-bar ist
 *
 * `NULL` heißt **„folgt dem Zeiger"** (`AppRelease.Current`) und ist der
 * Normalfall: eine Organisation, die nur ein Backend gewählt hat, soll nicht
 * auch noch eine Version festgenagelt bekommen. Ein gesetzter Wert ist die
 * Ausnahme (Canary) — dieselbe additive Logik wie bei `OrgReleaseOverride`.
 *
 * ## Warum kein Fremdschlüssel
 *
 * `AppBackendBranch` ist das **Angebot**, `OrgAppDeployment` die **Wahl**. Ein
 * FK mit `ON DELETE RESTRICT` würde den Schutz schon in der Datenbank
 * erzwingen — verlockend, aber dieses Schema führt **keinen einzigen**
 * Fremdschlüssel (geprüft: `initTables.sql` und alle Migrationen). Ein
 * Alleingang hier hätte zur Folge, dass Löschreihenfolgen plötzlich von der
 * DB abhängen. Der Schutz liegt deshalb im Code
 * (`registryService.saveBackendBranches`: ein Zweig wird nur entfernt, wenn ihn
 * keine Zuordnung mehr nennt) — und ist damit auch testbar.
 *
 * ## Auflösung (siehe `registryService.resolveRelease`)
 *
 * > **Abgelöst durch `20261004-release-name-lookup`.** Es gibt keine Stufen
 * > mehr: die Auflösung ist ein Nachschlagen per **Name**
 * > (`OrgAppDeployment.Version`, sonst `stable`), und die Backends kommen
 * > ausschließlich aus `AppBackendBranch` — `AppRelease.Backends` entfällt.
 *
 * ```
 * 1. OrgAppDeployment[AppKey][OrgUID].Version   → festgenagelt, sonst
 * 2. OrgReleaseOverride[AppKey][OrgUID]         → Canary, sonst
 * 3. AppRelease[AppKey].Current                 → der Zeiger, sonst
 * 4. nichts                                     → kein Release
 *
 * Backends: OrgAppDeployment[AppKey][OrgUID].Branch → AppBackendBranch.Backends
 *           sonst AppRelease.Backends (Rückfall während der Migration)
 * ```
 *
 * ## Herkunft der Vorlage (initTables.sql)
 *
 * Wie bei den Registry-Typen liegt die Struktur auch in `initTables.sql`, damit
 * frische Installationen nicht auf die Migration warten müssen. `IF NOT EXISTS`
 * macht das Zusammenwirken idempotent.
 *
 * @see PLAN.md §6.9 — Backends sind ein eigener Vertrag und eine Admin-Wahl
 */

/**
 * `(AppKey, Branch)` als Primärschlüssel: ein Zweig ist pro App eindeutig.
 *
 * `Branch` ist ein **freier, kurzer Name** (`test`, `prod`, `canary`) — bewusst
 * kein `enum`: ein neuer Zweig darf keine Schemaänderung sein, sonst wäre die
 * Flexibilität, um die es hier geht, wieder weg. Die Form wird beim Schreiben
 * geprüft (`registryTypes.isValidBranch`).
 *
 * `Backends` ist `NOT NULL`: ein Zweig **ohne** URLs wäre keine gültige Wahl.
 * Das ist der Unterschied zu `AppRelease.Backends` (dort `NULL`-bar, weil es der
 * Rückfall während der Migration ist).
 */
const CREATE_APP_BACKEND_BRANCH = `
CREATE TABLE IF NOT EXISTS \`AppBackendBranch\` (
  \`AppKey\`    VARCHAR(64) NOT NULL,
  \`Branch\`    VARCHAR(64) NOT NULL,
  \`Backends\`  JSON        NOT NULL,
  \`UpdatedAt\` TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (\`AppKey\`, \`Branch\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;

/**
 * Eine Organisation kann pro App **eine** Zuordnung haben — daher der
 * Primärschlüssel `(AppKey, OrgUID)`. Ein zweites „Deployment" derselben App für
 * dieselbe Organisation gäbe es nur, wenn ein Host auf zwei Apps zeigte; das ist
 * bereits über den Host-Konflikt ausgeschlossen (§6.6).
 *
 * `OrgUID` folgt dem Haus-Format `BINARY(16)` und wird über `U_UUID2BIN(?)`
 * adressiert (41-stellige `UUID-…`-Form, siehe `registryTypes`).
 *
 * Kein `Current`-Analogon: die Zuordnung ist nicht „die aktuelle", sie **ist**
 * die Zeile. Zeigt sie auf keine Version, folgt die Organisation dem Zeiger.
 */
const CREATE_ORG_APP_DEPLOYMENT = `
CREATE TABLE IF NOT EXISTS \`OrgAppDeployment\` (
  \`AppKey\`    VARCHAR(64) NOT NULL,
  \`OrgUID\`    BINARY(16)  NOT NULL,
  \`Version\`   VARCHAR(64) NULL,
  \`Branch\`    VARCHAR(64) NOT NULL,
  \`UpdatedAt\` TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (\`AppKey\`, \`OrgUID\`),
  KEY \`idx_branch\` (\`AppKey\`, \`Branch\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;

export const migrate = async ({ query }) => {
    await query(CREATE_APP_BACKEND_BRANCH, []);
    await query(CREATE_ORG_APP_DEPLOYMENT, []);
};