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, []);
};