Module: scripts/orgTransfer

orgTransfer.js — Export / Import / Netzwerk-Kopie genau EINER Organisation aus der members-MariaDB. Problem: Die members-Datenbank ist mandantenfähig, aber nicht über eine OrgUID-Spalte getrennt, sondern als Objekt-Graph. Eine Organisation ist ein `group`-Objekt mit `Data.root = true`, an dem alle ihre Objekte über "Propagations-Links" hängen (member, memberA, memberS, memberG, memberSys, member0, memberGA). Ein mysqldump der ganzen DB ist für einen Umzug einer einzelnen Organisation also unbrauchbar. Dieses Skript berechnet den Objekt-Scope einer Organisation rekursiv, exportiert die Kern-Tabellen als einspielbares SQL und kann das Ergebnis wahlweise in eine Datei schreiben, direkt in eine andere Datenbank streamen (Netzwerk-Kopie) oder aus einer Datei wieder einspielen. node src/scripts/orgTransfer.js inspect --org node src/scripts/orgTransfer.js export --org [Optionen] node src/scripts/orgTransfer.js import --in [Optionen] node src/scripts/orgTransfer.js copy --org --to-db [Optionen] Als `--org` darf jede Objekt-UID übergeben werden: ist es nicht selbst die Organisation, wird die zugehörige Organisation über die Propagations-Links aufgelöst (z.B. eine Person -> ihre Organisation). UUIDs werden über die DB-Funktionen `UUID2BIN()` / `BIN2UUID()` konvertiert (dieselben, die auch das Backend benutzt). Die Ziel-Datenbank muss diese Funktionen besitzen — sie kommen aus `initFunctions.sql`. Siehe `--help` für alle Optionen.
Source:

Classes

InsertBatcher
StatementSplitter

Members

(inner, constant) EXTRA_SCOPES

Zusätzliche, explizit anzufordernde Scope-Erweiterungen. Der Propagations-Scope enthält nur die "eigenen" Objekte der Organisation. Bestimmte Objekte werden von ihnen nur *referenziert* und fehlen dadurch — ohne sie funktioniert aber z.B. eine dynamische Liste nicht: - `filters` : `include`/`exclude`/`intersect`-Objekte einer `dlist`. Sie sind das ZIEL eines `dynamic`-Links, werden also nur über eine Vorwärts-Verfolgung gefunden. Im Test fehlten 129 Filter-Objekte bei ~36k Referenzen. - `templates` : Vorlagen/Kataloge (`achievementT`, `eventT`, `actionT`, `function`, `eventJobT`, ...) — in beide Richtungen verlinkt. `outgoing` folgt den Links von Scope-Objekten weg (referenzierte Ziele), `incoming` folgt Links in den Scope hinein. Beides läuft bis zum Fixpunkt, Fremd-Organisationen werden dabei nicht hineingezogen.
Source:

(inner, constant) NUMBER_RE

Nur echte Nutzdaten durchlassen (Schutz gegen kaputte Treiber-Werte).
Source:

(inner, constant) NUMERIC_TYPES

Datentypen, die als nackte Zahl ausgegeben werden.
Source:

(inner, constant) ORG_OWNED_MAX_ROUNDS

Zugehörigkeit über `UIDBelongsTo` wird **transitiv** verfolgt. `UIDBelongsTo` ist laut Schema Inheritance/Ownership — die Aussage „gehört diesem Objekt". Sie ist damit ein Graph-Rand wie ein Link, nur dass der Export ihm bisher gar nicht folgte. Zwei Fälle fallen dadurch heraus: 1. **Eltern-Objekte statt Organisation.** `include`/`exclude`/`intersect` hängen an ihrem `list`/`group`-Elternobjekt, nicht an der Organisation (gemessen: 155 an `list`, 142 an `group`). `action`/`eventJobT` hängen an `eventT`. Ein Prädikat `UIDBelongsTo = ` sieht davon nichts. 2. **Tiefer als eine Ebene.** Gemessen in der Dev-DB: 79 Objekte auf Tiefe 2 (67 `eventJobT` + 12 `action`, alle an einem `eventT`), verteilt auf eine Organisation. `--include filters,templates` hilft dort nicht, weil der `templates`-Scope *Links* folgt und diese Objekte gar keine tragen. Deshalb wird bis zum Fixpunkt wiederholt, nicht einmalig. Ein einzelner `INSERT ... JOIN __scope` pro Runde, Abbruch bei `affectedRows = 0`; die Obergrenze ist nur ein Sicherheitsnetz gegen einen Zyklus, der durch `INSERT IGNORE` ohnehin nicht entstehen kann. **Guard:** Sub-Organisationen (`Data.root = true`) werden nicht hineingezogen — die sind eigenständige Organisationen mit eigenem Export. Dieselbe Regel benutzt die Link-Closure unten.
Source:
Link-Typen, über die Zugehörigkeit zur Organisation propagiert wird. Dies sind exakt die Typen aus `getOrganizationForObject()` im Backend (`src/utils/organizationUtils.js`) — dort wird dieselbe Semantik genutzt, um zu einem Objekt seine Organisation zu finden. Umgekehrt aufgezogen ergeben sie den vollständigen Objekt-Bestand einer Organisation.
Source:

(inner, constant) SCOPE_TABLES

Optionale Scope-Flags -> Tabellen, die sie mitbringen.
Source:

(inner, constant) STRING_TYPES

Datentypen, die als String-Literal ausgegeben werden.
Source:

(inner, constant) TABLE_SPECS

Tabellen, die exportiert werden können. `scope`: 'org' — über die Objekt-UID (`__scope`) 'member' — Member-Zeilen, die von Objekten des Scopes benutzt werden 'accounting' — über `Transactions.UIDOrganization` 'events' — über `eventLog.UIDorga` `always`: gehört zum Kern-Export (Default-Tabellensatz).
Source:

Methods

(async, inner) applyDump(conn, readable, opts)

Dump-Statements auf eine Verbindung ausspielen.
Parameters:
Name Type Description
conn any Ziel-Verbindung
readable NodeJS.ReadableStream Dump-Quelle
opts object
Source:

(async, inner) assertTargetReady()

Voraussetzungen der Zieldatenbank prüfen.
Source:

(inner) bufferToBitString()

Buffer (bit) -> Dezimalstring.
Source:

(async, inner) buildContext()

Scope + Metadaten der Quelle aufbauen.
Source:

(async, inner) buildScope()

Objekt-Scope einer Organisation berechnen und in `__scope` materialisieren. Der Scope wird als TEMPORARY TABLE gehalten: er wird von jeder Tabellen- Query gebraucht und ist mit ~250k Zeilen zu groß für IN-Listen. Der rekursive Arm expandiert nur von Objekten aus, die entweder die Start-Organisation selbst sind oder keine Organisation sind. Dadurch kann die Traversierung nicht in eine andere Organisation "überlaufen", falls deren Root über einen Propagations-Link erreichbar wäre.
Source:

(inner) buildSelect()

SELECT-Ausdrücke bauen. Die Konvertierung passiert in SQL, nicht in JS: `BIN2UUID()` liefert den lesbaren String, `HEX()` die Binärdaten, `ST_AsWKB()`/`ST_SRID()` die Geometrie. Damit gibt es keine Zeitzonen- oder Encoding-Überraschungen durch den Node-Treiber.
Source:

(async, inner) checkTargetSchema()

Prüfen, ob die Zieldatenbank die im Dump verwendeten Tabellen und Spalten besitzt. Der Export liest die Spalten aus `information_schema` der Quelle. Ist das Ziel eine ältere Migrationsstufe, fehlen dort Spalten — ohne diesen Check bricht das Einspielen mitten im Dump mit einem kryptischen "Unknown column" ab.
Source:

(inner) defaultOutName()

Dateiname für den Dump.
Source:

(inner) describeConn()

Verbindungsdaten für's Log (ohne Passwort). Wichtig als Sicherheitsnetz: zeigt immer die *tatsächlich* benutzte Verbindung, nicht die aus der Umgebung — sonst könnte ein Export aus Prod versehentlich wie ein Export aus der Test-DB aussehen (oder umgekehrt).
Source:

(inner) encodeValue()

Einen Treiber-Wert in ein SQL-Literal umwandeln. UUIDs gehen über `UUID2BIN()` — die Gegenrichtung zu `BIN2UUID()` im SELECT und die vom Projekt vorgegebene Konvertierung (siehe `backend-uuid-approach`-Regel).
Source:

(inner) footerSql()

Dump-Fuß.
Source:

(inner) formatDateLocal()

Notfall-Formatierung für einen `Date`, falls der Treiber doch castet. Normalerweise kommt das nicht vor (`dateStrings: true` in openConnection) — aber ein stillschweigend verschobener Zeitstempel wäre schlimmer als eine lokale Formatierung.
Source:

(inner) headerSql()

Dump-Kopf.
Source:

(inner) human()

Bytes menschenlesbar.
Source:

(inner) id()

Bezeichner quoten.
Source:

(inner) isEffectivelyEmpty()

Ist das Statement nur ein Kommentar/Whitespace?
Source:

(inner) isRoot()

`Data.root` kommt je nach Zugriffsweg als Zahl, String oder boolean.
Source:

(async, inner) loadColumns()

Alle Spalten der gewünschten Tabellen aus `information_schema` holen und klassifizieren. Damit bleibt das Skript schema-agnostisch: neue Spalten werden automatisch mitgenommen, generierte Spalten automatisch ausgelassen.
Source:

(async, inner) loadSecretsPreservingEnv() → {Promise.<Array.<string>>}

Secrets laden — aber explizite ENV-Vorgaben gewinnen. `loadSecretsFromVault()` schreibt seine Werte **bedingungslos** nach `process.env`, würde ein vorgesetztes `DB_HOST` also überschreiben. Deshalb wird die Umgebung vorher gesichert und danach wieder hergestellt: Treiber-Default < Vault (`GIT_SECRET_PATH`) < ENV < `--from-*` / `--to-*` So kann man eine abweichende Quelle bequem per `-e DB_HOST=…` mitgeben, ohne die Passwort-Flags auf der Kommandozeile zu brauchen.
Source:
Returns:
Namen der Schlüssel, die Vorrang behalten haben
Type
Promise.<Array.<string>>

(async, inner) loadTableTypes()

Prüfen, ob eine Tabelle existiert und welcher Art sie ist. Nur BASE TABLE / SYSTEM VERSIONED werden exportiert — die Views `Objects`, `ObjectTargets` und `HasTarget` leiten sich aus den Basistabellen ab und dürfen nicht eingespielt werden.
Source:

(inner) normalizeOptions()

CLI-Argumente in einen Options-Objekt normalisieren.
Source:

(async, inner) openConnection(optsopt) → {Promise.<any>}

Verbindung öffnen. `dateStrings: true` ist hier wichtig: ohne das castet der mariadb-Treiber DATETIME/TIMESTAMP-Werte in JS-`Date`-Objekte und wendet dabei die lokale Zeitzone an. Für einen verlustfreien Dump brauchen wir den Roh-String des Servers ('2026-06-29 13:08:26.234419').
Parameters:
Name Type Attributes Description
opts object <optional>
host/user/password/database-Overrides
Source:
Returns:
mariadb-Verbindung (nicht gepoolt)
Type
Promise.<any>

(async, inner) openDumpReadable()

Quelldatei öffnen, gzip transparent entpacken.
Source:

(async, inner) openFileSink()

Schreib-Sink auf eine Datei (optional gzip).
Source:

(inner) parseArgs()

Minimaler Argument-Parser (`--key wert`, `--flag`, `--key=wert`).
Source:

(inner) parseDumpManifest() → {Map.<string, Array.<string>>}

Spalten-Manifest aus dem Dump-Kopf lesen. Format: `-- @manifest Member:UID,Display|ObjectBase:UID,Type,...`
Source:
Returns:
Type
Map.<string, Array.<string>>

(inner) parseDumpOrg()

Org-UID aus dem Dump-Kopf lesen (für den Vorab-Check).
Source:

(inner) pick()

CLI-Wert, falls gesetzt — sonst der ENV-Fallback.
Source:

(inner) planTables()

Auflösen, welche Tabellen mit welchen Spalten tatsächlich geschrieben werden. Das Ergebnis steht sowohl im Dump-Manifest als auch im Insert-Loop — beides muss dieselbe Sicht haben.
Source:

(inner) printPurgeReport()

`--mode replace`-Report aufs Terminal.
Source:

(async, inner) purgeTargetOrg(conn, orgUid, opts, tablesopt) → {Promise.<Array.<{table: string, removed: number}>>}

Vorhandene Organisation im Ziel entfernen — der „tolerante" Import-Pfad. **Warum das überhaupt nötig ist.** `checkOrgaExists()` (config/initOrga.js) läuft beim Backend-Start für **jede** Organisation, die eine Config-Datei (`config/admin/UUID-.yaml`) hat, und legt sie an, falls sie fehlt: Org-Objekt, BOT-User (`extern`), `@@SuperAdmin`-Job und Superuser-Filter — mit Defaults, also einem Platzhalter-Namen, nicht mit den echten Daten. Ein Dump-Import trifft dieses Ziel also praktisch **nie leer** an. Und ein „drüberbügeln" kann nicht funktionieren, weil der Stub seine System-Objekte bei jeder Erzeugung mit **neuen Zufalls-UIDs** anlegt (`SELECT UIDV1() AS UIDExtern, UIDV1() AS UIDJob, UIDV1() AS UIDFilter` in initOrga.js). Diese UIDs können nie mit den gleichnamigen Objekten aus dem Dump zusammenfallen: es blieben doppelte BOT-User, zwei `@@SuperAdmin`-Jobs und zwei Superuser-Filter zurück. Vorher scheitert der Import schon am Primärschlüssel — `ObjectBase` ist auf `(UID, ValidUntil)` geschlüsselt, und die offene Zeile (`ValidUntil` = 2106-02-07) ist bereits belegt; `Member` (`UID`) und `Visible` (`UID, UIDUser`) ebenso. **Was gelöscht wird.** Die Closure wird im Ziel mit denselben Regeln aufgebaut wie beim Export (`buildScope`), dann Zeile für Zeile entfernt — und nur für die Tabellen, die der Dump auch wieder einspielt. Was der Dump nicht mitbringt, wird nicht angefasst. Bei system-versionierten Tabellen (`ObjectBase`, `Links`) beendet das `DELETE` nur die offene Version (`ValidUntil` = jetzt); die Historie bleibt erhalten. Damit ist der Primärschlüssel frei und der anschließende `INSERT` des Dumps passt wieder. Abhängige Tabellen werden **vor** ihren Eltern gelöscht (`order` absteigend), sonst räumt z.B. `TransactionLines` über einen Subselect auf schon gelöschte `Transactions` nichts mehr weg.
Parameters:
Name Type Attributes Default Description
conn any Ziel-Verbindung
orgUid string Organisations-UID
opts object Optionen (nutzt `include`, `tables`, `dryRun`)
tables Array.<string> | null <optional>
null Tabellen des Dumps; `null` = Default-Satz
Source:
Returns:
Type
Promise.<Array.<{table: string, removed: number}>>

(inner) quote()

SQL-String-Literal mit MySQL-Backslash-Escapes (wie mysqldump).
Source:

(async, inner) readHead()

Erste Bytes einer Datei als Text (für den Kopf-Kommentar).
Source:

(async, inner) resolveOrganization()

Organisation zu einer beliebigen Objekt-UID bestimmen. Ist die UID selbst eine Organisation (`Data.root = true`), wird sie direkt verwendet. Andernfalls wird über die Propagations-Links die zugehörige Organisation gesucht — dieselbe Query wie `getOrganizationForObject()` im Backend.
Source:
Returns:

(async, inner) runDump()

Lesbaren Stream zeilenweise durch den Splitter schicken.
Source:

(async, inner) scopeReport()

Kurzreport über den Inhalt des Scopes (für inspect und export).
Source:

(inner) secs()

Sekunden mit einer Nachkommastelle.
Source:

(inner) selectedTables()

Tabellen für diesen Lauf bestimmen.
Source:

(inner) shortUuid()

Kurzform einer UUID für Dateinamen.
Source:

(inner) sourceOptions()

Auflösung der Org-UID aus CLI/Env-Overrides.
Source:

(async, generator, inner) streamRows()

Ergebnis einer Query als Stream durchreichen. Wichtig für `Visible` (~800k Zeilen) und `Links`: die Treiber-Query würde sonst alle Zeilen gleichzeitig im Speicher halten. `queryStream` respektiert Backpressure.
Source:

(inner) tableFilter()

WHERE-Klausel einer Tabelle.
Source:

(async, inner) targetHasOrg()

Prüfen, ob die Organisation im Ziel schon existiert.
Source:

(inner) wantProgress()

Fortschrittszeilen nur auf einem echten Terminal ausgeben (\r in Pipes ist Müll).
Source: