Source: config/migrations/20260928-app-release.js

export const id = '20260928-app-release';
export const name = 'App-Release: AppRelease und OrgReleaseOverride';

/**
 * Auslieferung von Apps als **Zeiger** statt als Deployment.
 *
 * ## Warum zwei Tabellen und nicht eine Spalte `OrgUID`
 *
 * Ein App-Deploy soll das Umschreiben eines Zeigers sein — kein Container, der
 * neu startet. `AppRelease` hält dazu pro `(AppKey, Version)` das S3-Präfix und
 * markiert über `Current` genau eine Zeile als die ausgelieferte. Der Normalfall
 * ist damit eine Zeile pro App.
 *
 * Canary ist die **Ausnahme**: eine Organisation bekommt eine andere Version.
 * Das ist eine 1:n-Beziehung und liegt deshalb in einer eigenen Tabelle
 * (`OrgReleaseOverride`) — nicht als `OrgUID`-Spalte hier. Der Unterschied ist
 * nicht kosmetisch: eine Spalte hätte für jede Organisation eine eigene
 * `AppRelease`-Zeile erzwungen, also Vervielfachung der Release-Zeilen und eine
 * zweite Wahrheit darüber, welches Release „das aktuelle" ist. So ist Canary
 * additiv: **kein Eintrag = kein Canary**, und wer keinen Eintrag hat, folgt
 * weiter dem Zeiger.
 *
 * ## Frontend und Backends sind ein Release
 *
 * `Backends` liegt bewusst **in** der Release-Zeile und nicht in einer eigenen
 * Einstellung. Eine neue Frontend-Version ruft in der Regel neue Endpunkte; würde
 * man das Frontend bewegen können, ohne das Backend zu bewegen, entstünde genau
 * der Bruch, den Canary verhindern soll (neue Oberfläche gegen alten Backend).
 * In einer Zeile ist die Kopplung mechanisch statt eine Frage der Disziplin.
 * Der Gegenfall bleibt ausdrückbar: ein reiner Backend-Canary ist eine
 * Release-Zeile mit **unverändertem** `Prefix` und neuen `Backends`.
 *
 * `Backends` ist `NULL`-bar: solange nicht jedes Release es gesetzt hat, liefert
 * `env.js` unverändert die Werte aus dem Deployment — dieselbe Dual-Read-Logik
 * wie beim Registry, nur auf `env.js` angewendet.
 *
 * ## Auflösung (siehe `registryService.resolveRelease`)
 *
 * ```
 * 1. OrgReleaseOverride[AppKey][OrgUID]  → dieses Release, sonst
 * 2. AppRelease[AppKey].Current          → der Zeiger, sonst
 * 3. nichts                              → kein Release
 * ```
 *
 * ## Herkunft der Vorlage (initTables.sql)
 *
 * Wie bei den Registry-Enum-Typen liegt die Struktur auch in `initTables.sql`,
 * damit frische Installationen nicht auf die Migration warten müssen. Beide Wege
 * erzeugen dieselben Tabellen; `IF NOT EXISTS` macht das Zusammenwirken
 * idempotent.
 *
 * @see PLAN-app-registry.md §6.2 — AppRelease / OrgReleaseOverride
 */

/**
 * `AppKey` ist die App-Kennung aus dem Registry (`Data.appId` des `app`-Objekts),
 * nicht eine UID: die Registry vergibt `appId` als stabilen Schlüssel
 * (§ registryTypes), und genau er adressiert ein Release über Organisationen
 * hinweg. Die App-Objekte selbst bleiben in `ObjectBase`.
 */
const CREATE_APP_RELEASE = `
CREATE TABLE IF NOT EXISTS \`AppRelease\` (
  \`AppKey\`   VARCHAR(64)  NOT NULL,
  \`Version\`  VARCHAR(64)  NOT NULL,
  \`Prefix\`   VARCHAR(255) NOT NULL,
  \`Backends\` JSON         NULL,
  \`Current\`  TINYINT(1)   NOT NULL DEFAULT 0,
  PRIMARY KEY (\`AppKey\`, \`Version\`),
  KEY \`idx_current\` (\`AppKey\`, \`Current\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;

/**
 * `(AppKey, OrgUID)` als Primärschlüssel: eine Organisation kann pro App nur auf
 * **ein** Canary-Release zeigen. Genau eine Zeile ist damit der Canary, und ein
 * Rollback ist ein `DELETE`.
 *
 * `OrgUID` folgt dem Haus-Format `BINARY(16)` und wird über `U_UUID2BIN(?)`
 * adressiert (41-stellige `UUID-…`-Form, § registryTypes: Cast-Regeln).
 *
 * `AddedAt` steht ohne `ON UPDATE` — es ist der Zeitpunkt, ab dem der Canary
 * gilt, und soll sich beim Lesen nicht verändern.
 */
const CREATE_ORG_RELEASE_OVERRIDE = `
CREATE TABLE IF NOT EXISTS \`OrgReleaseOverride\` (
  \`AppKey\`  VARCHAR(64) NOT NULL,
  \`OrgUID\`  BINARY(16)  NOT NULL,
  \`Version\` VARCHAR(64) NOT NULL,
  \`AddedAt\` TIMESTAMP   NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (\`AppKey\`, \`OrgUID\`),
  KEY \`idx_version\` (\`AppKey\`, \`Version\`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`;

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