diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0dac205..2ea2a81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,8 +34,8 @@ jobs: - name: Run strict test suite env: MXRB_COVERAGE: "1" - MXRB_LINE_COVERAGE_MIN: "98" - MXRB_BRANCH_COVERAGE_MIN: "95" + MXRB_LINE_COVERAGE_MIN: "100" + MXRB_BRANCH_COVERAGE_MIN: "100" MXRB_ACCEPTANCE_MPRS: /tmp/mxcli-sudoku/Sudoku/Sudoku.mpr run: bundle exec rspec @@ -59,7 +59,27 @@ jobs: ruby-version: "4.0" bundler-cache: true + - name: Restore pinned ONNX model cache + uses: actions/cache@v4 + with: + path: ~/.cache/informers + key: informers-${{ runner.os }}-1.3.0-all-MiniLM-L6-v2-1110a243fdf4706b3f48f1d95db1a4f5529b4d41 + + - name: Warm pinned ONNX model cache + run: | + for attempt in 1 2 3 4 5; do + if bundle exec ruby -Ilib -e 'require "mxrb"; Mxrb::Semantic::OnnxEmbedder.new.dimension'; then + exit 0 + fi + if [ "$attempt" = 5 ]; then + exit 1 + fi + sleep $((attempt * 10)) + done + - name: Run real ONNX semantic-search smoke test + env: + INFORMERS_OFFLINE: "1" run: bundle exec rspec spec/semantic_search_spec.rb sqlite-vec: diff --git a/README.md b/README.md index 2830efb..ebc6ec2 100644 --- a/README.md +++ b/README.md @@ -154,13 +154,13 @@ and persisted state through entity/XPath count assertions. ```sh bundle exec rspec -MXRB_COVERAGE=1 bundle exec rspec +MXRB_COVERAGE=1 MXRB_LINE_COVERAGE_MIN=100 MXRB_BRANCH_COVERAGE_MIN=100 bundle exec rspec bundle exec ruby script/branch_report.rb bundle exec rubocop ``` -The enforced suite currently has 100% line and branch coverage. The branch -report lists any regression by source file and line. +CI and the local coverage helper enforce 100% line and 100% branch coverage. +The branch report lists any regression by source file and line. Architecture and deeper writing guidance are available in [the English documentation](docs/en-US/README.md). The complete documentation diff --git a/bin/mxrb b/bin/mxrb index c97e642..17344b0 100755 --- a/bin/mxrb +++ b/bin/mxrb @@ -256,6 +256,50 @@ when "widgets" puts "[mxrb] Synchronized widget schemas with #{result.mx_path}" puts "[mxrb] Generated #{result.project}" +when "frontend" + frontend_usage = <<~USAGE + Usage: mxrb frontend migrate FILE.mpr [--apply] [--json] + + Preview frontend widget, layout-row, and design-property migrations. + The MPR is changed only when --apply is provided and the plan is safe. + USAGE + action = ARGV.shift + if action.nil? || %w[-h --help help].include?(action) + puts frontend_usage + exit(action.nil? ? 1 : 0) + end + abort "Unknown frontend action #{action.inspect}; use migrate" unless action == "migrate" + if %w[-h --help].include?(ARGV.first) + puts frontend_usage + exit 0 + end + path = ARGV.shift or abort "Usage: mxrb frontend migrate FILE.mpr [--apply] [--json]" + apply = !ARGV.delete("--apply").nil? + json = !ARGV.delete("--json").nil? + abort "Unknown arguments: #{ARGV.join(' ')}" unless ARGV.empty? + plan = Mxrb::Frontend::Migrator.plan(path) + plan.apply! if apply && plan.safe? + payload = { + version: plan.version, changes: plan.changes.size, widgets: plan.widgets, + layout_rows: plan.layout_rows, design_properties: plan.design_properties, + issues: plan.issues.map(&:to_h), safe: plan.safe?, applied: plan.applied? + } + if json + puts JSON.pretty_generate(payload) + else + puts "Version : #{payload[:version]}" + puts "Changes : #{payload[:changes]}" + puts "Widgets : #{payload[:widgets]}" + puts "Layout rows : #{payload[:layout_rows]}" + puts "Design properties : #{payload[:design_properties]}" + puts "Safe : #{payload[:safe]}" + puts "Applied : #{payload[:applied]}" + plan.issues.each do |issue| + puts "[#{issue.kind.to_s.upcase}] #{issue.unit_id}#{issue.path}: #{issue.message}" + end + end + exit 1 unless plan.safe? + when "export" source = ARGV.shift or abort "Usage: mxrb export " output = ARGV.shift or abort "Usage: mxrb export " @@ -1037,7 +1081,7 @@ when "team-server" when "marketplace" marketplace_usage = <<~USAGE - Usage: mxrb marketplace ... + Usage: mxrb marketplace ... marketplace login --pat-file FILE [--no-verify] Recommended: store only a path and read the PAT from FILE or a Ruby .env. @@ -1052,15 +1096,21 @@ when "marketplace" marketplace import FILE.mpk --mpr FILE.mpr Import an official Mendix module package directly with Ruby/SQLite/BSON. marketplace pull NAME[@VERSION] --mpr FILE.mpr [--mendix-version VERSION] - Download from the official Marketplace and import directly into the MPR. + Download official Module, Service, or Widget content and install it transactionally. marketplace pull github:ORG/REPO --mpr FILE.mpr Use a public GitHub release instead of the official Marketplace. + marketplace update NAME[@VERSION] [--mpr FILE.mpr] [--apply] + Preview a reference-safe official update; mutate only with --apply. + marketplace remove NAME [--mpr FILE.mpr] [--apply] + Preview reference-safe removal; mutate only with --apply. + marketplace dependencies NAME [--mpr FILE.mpr] [--apply|--apply-resolved] + Resolve module references and WidgetIds; verify every downloaded MPK identity. marketplace audit [--target DIR] [--mendix-version VERSION] Check locked official components for updates and known vulnerabilities. marketplace list|verify [--target DIR] Inspect or verify .mxrb/marketplace.lock.json and cached packages. - Module import does not invoke mx, mxcli, Studio Pro, or the Model SDK. + Module/widget installation does not invoke mx, mxcli, Studio Pro, or the Model SDK. USAGE marketplace_login_usage = <<~USAGE Usage: @@ -1103,6 +1153,19 @@ when "marketplace" end mpr = option.call('--mpr') target = option.call('--target') || (mpr ? File.dirname(File.expand_path(mpr)) : Dir.pwd) + plan_state = lambda do |plan| + next 'partial applied' if plan.applied? && !plan.safe? + next 'applied' if plan.applied? + + plan.safe? ? 'safe preview' : 'blocked preview' + end + print_lifecycle_plan = lambda do |plan| + state = plan_state.call(plan) + version = plan.target_version ? " -> #{plan.target_version}" : '' + puts "[mxrb] #{plan.action} #{plan.name} #{plan.installed_version}#{version}: #{state}" + plan.changes.each { puts " change: #{_1}" } + plan.blockers.each { puts " blocker: #{_1}" } + end begin case action when 'login' @@ -1216,6 +1279,40 @@ when "marketplace" puts "[mxrb] Imported #{result.package.name} into #{result.destination}" puts "[mxrb] #{result.units} MPR units written without Studio Pro" if result.respond_to?(:units) puts "[mxrb] SHA-256 #{result.sha256}" + when 'update' + identifier = ARGV.shift or + abort 'Usage: mxrb marketplace update [--mpr FILE] [--apply]' + name, version = identifier.split('@', 2) + plan = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:).update_official( + name, version:, + mendix_version: option.call('--mendix-version'), + allow_vulnerable: !ARGV.delete('--allow-vulnerable').nil?, + apply: !ARGV.delete('--apply').nil? + ) + print_lifecycle_plan.call(plan) + exit 1 unless plan.safe? + when 'remove' + identifier = ARGV.shift or + abort 'Usage: mxrb marketplace remove [--mpr FILE] [--apply]' + plan = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:).remove( + identifier, apply: !ARGV.delete('--apply').nil? + ) + print_lifecycle_plan.call(plan) + exit 1 unless plan.safe? + when 'dependencies' + identifier = ARGV.shift or + abort 'Usage: mxrb marketplace dependencies [--mpr FILE] [--apply|--apply-resolved]' + apply = !ARGV.delete('--apply').nil? + apply_resolved = !ARGV.delete('--apply-resolved').nil? + abort '--apply and --apply-resolved are mutually exclusive' if apply && apply_resolved + plan = Mxrb::OfficialMarketplace::Installer.new(target:, mpr:).dependencies( + identifier, apply:, apply_resolved: + ) + state = plan_state.call(plan) + puts "[mxrb] dependencies #{plan.root}: #{state}" + plan.changes.each { puts " change: #{_1}" } + plan.blockers.each { puts " blocker: #{_1}" } + exit 1 unless plan.safe? when 'list' packages = Mxrb::OfficialMarketplace.lock(target).fetch('packages') packages.each { |name, entry| puts "#{name}\t#{entry['version']}\t#{entry['source']}" } @@ -1402,6 +1499,8 @@ else --output FILE Uses installed Runtime files; never calls mx/mxbuild mda inspect|compare Inspect or diff MDA contents and metadata widgets sync Synchronize pluggable widget schemas + frontend migrate Preview frontend schema/layout/theme migration + [--apply] [--json] Apply only a safe plan or emit structured output export Convert an MPR to editable Ruby validate Check MPR structure, hashes and v2 contents preflight [--json] Audit native compiler/runtime compatibility diff --git a/docs/de-DE/compiler.md b/docs/de-DE/compiler.md index e00ce75..13d4a26 100644 --- a/docs/de-DE/compiler.md +++ b/docs/de-DE/compiler.md @@ -14,8 +14,8 @@ Abhängigkeiten und ein Runtime-kompatibel geordnetes BSON-`model.mdp` werden ebenfalls erzeugt. Der Bootstrap besitzt auditierte Seeds und Kataloge für 6.10.8, 7.5.0, -7.17.0, 9.6.1.29396 und 11.12.1. Die Familien 6.x, 7.x, 9.x und 11.x wählen -einen kompatiblen Compiler-Seed; 5.x, 8.x und 10.x schlagen ohne auditierten +7.17.0, 9.6.1.29396, 10.24.0.73019 und 11.12.1. Die Familien 6.x, 7.x, 9.x, 10.x und 11.x wählen +einen kompatiblen Compiler-Seed; 5.x und 8.x schlagen ohne auditierten Seed geschlossen fehl. Die Familie gilt nur für das Compiler-Schema. Die Runtime muss exakt zum Patch des MPR passen. @@ -24,9 +24,10 @@ mise, kompiliert `javasource/**/*.java` gegen Runtime- und Projektbibliotheken und schreibt ein deterministisches OSGi-`project.jar`. Für VetClinic wurden 183 Quelldateien zu 249 Klassen kompiliert. -`WebBundleBuilder` wählt Dojo für 6/7, Dojo mit React-Wrapper für 9 und React -für 11. Der React-Pfad erzeugt Entry-/Seitenmodule, entpackt `.mpk` und ruft -Node/Rspack der Version direkt auf. Data Grid 2 wird für den Teilumfang aus XPath-Datenquelle +`WebBundleBuilder` wählt Dojo für 6/7, Dojo mit React-Wrapper für 9, den im +Projekt gewählten klassischen oder optimierten Client für 10 und React für 11. +Der React-Pfad erzeugt Entry-/Seitenmodule, entpackt `.mpk` und ruft direkt +Node/Rollup für 10 oder Node/Rspack für 11 auf. Data Grid 2 wird für den Teilumfang aus XPath-Datenquelle und Attributspalten kompiliert, einschließlich `operations.json`, Datenquelle und Attributtypen. Noch nicht übersetzte Typen oder Eigenschaftskombinationen erhalten einen DOM-Fallback in `web/mxrb-pages.json`; das VetClinic-Manifest ist @@ -144,14 +145,15 @@ Seitenbundles und fuhr sauber herunter. Danach bestand der Funktionstest ## Explizite Grenzen - alle 12 Stufen und saubere Web-Generierung sind für 6.10.8, 7.5.0, 7.17.0, - 9.6.1.29396 und 11.12.1 validiert; -- ein exakter nativer Boot ist für 11.12.1 belegt. Die installierten + 9.6.1.29396, 10.24.0.73019 und 11.12.1 validiert; +- ein exakter nativer Boot ist für 10.24.0.73019 und 11.12.1 belegt. Die installierten 6/7/9-Distributionen enthalten exakte Runtime-Bundles, aber keinen - PAD/Launcher; `db up` schlägt geschlossen fehl, statt den Java-21-Launcher + kompatiblen Launcher; `db up` schlägt geschlossen fehl, statt den Java-21-Launcher aus Version 11 einzusetzen; - Data Grid 1 ist abgedeckt, andere Dojo-Widgets bleiben explizite Manifestbefunde; -- der Mendix-11-React-Pfad besitzt saubere Preflights und vollständige +- der optimierte Mendix-10-React/Rollup-Pfad ist durch eine neu erstellte + Anwendung belegt; der Mendix-11-React-Pfad besitzt saubere Preflights und vollständige Rspack-Builds für LearnNow, MyFirstModule, SLATaskApp, Sudoku und VetClinic. Diese Fixture-Matrix belegt auditierte Verträge, keine universelle Kompatibilität; - native Web-Bundles werden erzeugt; React Native verwendet weiterhin diff --git a/docs/de-DE/native-runtime-quality-report.md b/docs/de-DE/native-runtime-quality-report.md index 14dd4c7..4904505 100644 --- a/docs/de-DE/native-runtime-quality-report.md +++ b/docs/de-DE/native-runtime-quality-report.md @@ -1,18 +1,19 @@ # Qualitätsbericht für nativen Build und Runtime -Datum: 1. August 2026. +Datum: 4. August 2026. ## Bestätigtes Ergebnis - keine funktionale Stufe ruft `mx`, `mxbuild`, `mxcli`, Studio Pro oder Model SDK auf; -- sauberer 12-Stufen-Build und Web-Generierung bestanden mit 6.10.8 (39 Seiten), 7.5.0 (45), 7.17.0 (66), 9.6.1.29396 (90) und 11.12.1; +- sauberer 12-Stufen-Build und Web-Generierung bestanden mit 6.10.8 (39 Seiten), 7.5.0 (45), 7.17.0 (66), 9.6.1.29396 (90), 10.24.0.73019 (optimierter Client) und 11.12.1; - die Round-trip-Matrix bestand mit sechs echten Fixtures von 5.21.4 bis 11.12.1: 1.506 Units, 1.734 Artefakte und 3.388 Referenzen; -- auditierte Compiler-Schemas/Seeds decken 6.x, 7.x, 9.x und 11.x ab. 5.x, 8.x und 10.x schlagen bei nativer Kompilierung geschlossen fehl; Runtime benötigt den exakten Patch; +- auditierte Compiler-Schemas/Seeds decken 6.x, 7.x, 9.x, 10.x und 11.x ab. 5.x und 8.x schlagen bei nativer Kompilierung geschlossen fehl; Runtime benötigt den exakten Patch; - Data Grid 1 deckt Datenbank/XPath/Microflow, Suche, Sortierung, Paging, Auswahl und auditierte Buttons ab. Data Grid 2 deckt XPath, Attributspalten und Create ab; -- Webprofile: Dojo in 6/7, Dojo/React-Hybrid in 9 und React in 11; +- Webprofile: Dojo in 6/7, Dojo/React-Hybrid in 9, projektabhängig klassisch/optimiert in 10 und React in 11; - Projects API inventarisierte 130 Apps. Drei echte Git-Projekte (`MyFirstModule`, `LearnNow Trainning Management`, `SLATaskApp`) bestanden validate → export → generate → validate → compare; - `MyFirstModule` wurde ohne proprietäre Builder als exaktes 11.12.1 erzeugt. Runtime synchronisierte 655 Operationen, erstellte den aktiven Admin `mx`, lieferte React-Shell und gestaltetes Login auf `127.0.0.1:18080` und zeigte die erwarteten Tabellen; -- finale QA: 821 Beispiele, null Fehler, 100% Zeilen (12.987/12.987), 100% Branches (4.611/4.611) und RuboCop ohne Verstöße in 205 Dateien. +- eine neu erstellte 10.24.0.73019-Anwendung mit optimiertem Client bestand offizielles `mx check` und `mxbuild`, natives Modell/Java/Rollup/Portable-Paket, 460 Datenbanksynchronisierungen, Readiness-Probe und HTTP 200; +- finale QA: 1.002 Beispiele, null Fehler, 100,00% Zeilen (16.917/16.917), 100,00% Branches (6.659/6.659) und RuboCop ohne Verstöße in 238 Dateien. ## Korrekturen aus dem Verbesserungsbericht @@ -35,8 +36,9 @@ Datum: 1. August 2026. Teacher-Gallery blieb strukturell vorhanden; - Nanoflow-Listen-/Objektquellen und Button-Aktionen werden in die echten Mendix-Clientverträge und funktionsreferenzierte Instruktionsprogramme - kompiliert. Der auditierte lineare Teilumfang ist getestet; nicht unterstützte - Clientknoten schlagen geschlossen fehl statt als Microflows zu erscheinen; + kompiliert. Der auditierte Graph-Teilumfang umfasst Entscheidungen, + Fehlerpfade, verschachtelte Nanoflows, JavaScript-Aktionen und serverseitige + Microflow-Aufrufe; nicht unterstützte Clientknoten schlagen geschlossen fehl; - echte Browserklicks bestätigten Home → Courses → Add → Save. Parametergestützte DataView/TextBox-Formulare rendern und speichern Werte; Create übergibt die GUID über `openForm2`, während die Commit-/Rollback-Autorisierung aus den Modulrollen @@ -48,10 +50,10 @@ Datum: 1. August 2026. 1. Andere Dojo-Widgets als Data Grid 1 bleiben in `web/mxrb-legacy-pages.json`; sie werden nicht als gerendert gemeldet. 2. Data Grid 2 deckt den auditierten XPath/Attribut/Create-Teil ab. Gallery deckt - XPath/Microflow und den auditierten linearen Nanoflow-Teil ab. Verzweigungen, - JavaScript-Aktionen, interne Microflow-Aufrufe und unsichere Parameterabbildungen - schlagen weiterhin geschlossen fehl. -3. Installierte 6/7/9-Distributionen enthalten exakte Bundles, aber keinen kompatiblen PAD/Launcher. Der 11-Launcher benötigt Java 21 und kann 9 unter Java 11 nicht sicher starten. Legacy-`db up` schlägt geschlossen fehl; nur der exakte 11.12.1-Boot wird behauptet. + XPath/Microflow und den auditierten Nanoflow-Graph-Teil ab. Nicht unterstützte + Graphformen, fehlende Referenzen, unsichere Parameterabbildungen und nicht + übersetzte Client-Instruktionen schlagen weiterhin geschlossen fehl. +3. Installierte 6/7/9-Distributionen enthalten exakte Bundles, aber keinen kompatiblen Launcher. Der 11-Launcher benötigt Java 21 und kann 9 unter Java 11 nicht sicher starten. Legacy-`db up` schlägt geschlossen fehl; exakter Boot wird nur für 10.24.0.73019 und 11.12.1 behauptet. 4. Runtime verwendet eine lokale Trial-/Developer-Lizenz mit erwarteter Zeitlimit-Warnung. 5. Cloud Build/Deploy/Pipelines/Backups sind optionale externe Prüfungen, nie Abhängigkeiten des nativen Kerns. diff --git a/docs/de-DE/platform-operations.md b/docs/de-DE/platform-operations.md index c91a621..6a4398e 100644 --- a/docs/de-DE/platform-operations.md +++ b/docs/de-DE/platform-operations.md @@ -17,6 +17,54 @@ mxrb migrate check . ist ohne `--apply` nur eine Vorschau. Migration erzeugt das Projekt temporär und vergleicht es mit dem aktuellen MPR; `check` schlägt bei Modelldrift fehl. +## Frontend-Migration und Abnahme + +```sh +mxrb frontend migrate App.mpr --json +mxrb frontend migrate App.mpr --apply --json +script/frontend_acceptance App.mpr -o frontend-round-trip.json +script/frontend_acceptance App.mpr --mxbuild /pfad/mxbuild -o frontend.json +script/frontend_acceptance App.mpr --mx /pfad/mx -o frontend-diagnostics.json +``` + +`mxrb frontend migrate` ist standardmäßig eine unveränderliche, fail-closed +Vorschau. Für Mendix 10 und 11 plant der Befehl Aktualisierungen installierter +Pluggable-Widget-Schemata, alter Layout-Zeilen-Gewichte und Design Properties +aus dem Paket-XML. `--apply` schreibt nur einen sicheren Plan in einer einzigen +MPR-Transaktion. Unbekannte konfigurierte Properties, eine nicht unterstützte +Generation oder eine nach der Vorschau geänderte Unit blockieren den Schreibzugriff. +Weder Vorschau noch Anwendung rufen Mendix-Werkzeuge auf. + +`script/frontend_acceptance` ist das reproduzierbare 10/11-Gate. Es validiert +Quell- und Rebuild-MPR, verlangt für beide einen kompatiblen nativen Preflight, +exportiert nach Ruby, baut neu auf, vergleicht die Modellstruktur und prüft +vollständige Asset-Inventare, Bytes, SHA-256-Prüfsummen und Marketplace- +Provenienz. Die Provenienzgrenze umfasst `.mxrb/marketplace.lock.json`, den +Paketcache und `.mxrb/marketplace-originals`; fehlende, geänderte oder +unerwartete Dateien lassen das Gate fehlschlagen. Die akzeptierte Renderer- +Matrix deckt jetzt Forms-Tabellen, `ListViewXPathSource`, Listen-Target- +Objekt-Properties, strukturierte Page-Variable-Mappings und Combo-box- +Enumerationen ab. Die Preflight-Basis sank unter 10.24 von 28 auf 0 und unter +11.12 von 20 auf 0 Befunde. + +Ohne `--mxbuild` lautet der Report-Scope `round_trip`, und `frontend_ready` +bleibt unbelegt. Mit `--mxbuild` dient MxBuild als schreibgeschütztes externes +Orakel und der Scope lautet `frontend`; es generiert, verändert oder repariert +das Projekt nie. Das Live-Orakel akzeptiert Erfolg nur, wenn MxBuild mit Status +null endet und ein nicht leeres MDA erzeugt; ein Toolchain-Fehler ohne gemeldete +Modellfehler schlägt geschlossen fehl. Am 4. August 2026 schloss die sichere +Migration die akzeptierte Matrix 10.24.0.73019 und 11.12.1 ab: Quelle und Rebuild lieferten null +MxBuild-Fehler, und beide Reports setzten `frontend_ready` auf `true`. + +`--mx` führt den offiziellen schreibgeschützten Checker mit Warnungen, +Deprecations und Best-Practice-Empfehlungen aus. Das Gate validiert die +Exit-Bitmaske und vergleicht normalisierte Signaturen von Quelle und Rebuild. +Die akzeptierte 10.24-Fixture hat 0 Fehler, 173 paketbezogene Warnungen, +0 Deprecations und 2 Kafka-Empfehlungen; 11.12 hat 0 Fehler, 10 paketbezogene +Warnungen, 0 Deprecations und dieselben 2 Empfehlungen. Die JSON-Dateien von +Quelle und Rebuild sind in beiden Generationen bytegleich; diese beobachtbaren +Paketdiagnosen sind kein MXRB-Round-trip-Drift. + ## Offizieller und Community-Mendix-Marketplace MXRB verwendet die dokumentierte Mendix Marketplace Content API. Das PAT @@ -30,6 +78,13 @@ mxrb marketplace search "Community Commons" mxrb marketplace show 170 mxrb marketplace versions 170 --mendix-version 11.12.1 mxrb marketplace pull 170 --mpr MeineApp.mpr +mxrb marketplace dependencies CommunityCommons --mpr MeineApp.mpr +mxrb marketplace dependencies CommunityCommons --mpr MeineApp.mpr --apply +mxrb marketplace dependencies CommunityCommons --mpr MeineApp.mpr --apply-resolved +mxrb marketplace update 170@3.5.0 --mpr MeineApp.mpr +mxrb marketplace update 170@3.5.0 --mpr MeineApp.mpr --apply +mxrb marketplace remove CommunityCommons --mpr MeineApp.mpr +mxrb marketplace remove CommunityCommons --mpr MeineApp.mpr --apply mxrb marketplace pull github:mendix/CommunityCommons mxrb marketplace import ./CommunityCommons.mpk --mpr MeineApp.mpr mxrb marketplace audit --target . --mendix-version 11.12.1 @@ -71,6 +126,47 @@ Updates und Schwachstellen. `login` validiert und speichert das PAT außerhalb des Projekts mit Modus `0600`. Das PAT wird nur an die exakten offiziellen Hosts `marketplace-api.mendix.com` und `marketplace.mendix.com` gesendet. +`marketplace dependencies` entdeckt ungelöste qualifizierte Modulreferenzen im +eingebetteten Paket-MPR, löst sie rekursiv über die offizielle API, lädt jeden +Kandidaten und akzeptiert ihn nur, wenn das MPK selbst die angeforderte +Modulidentität bestätigt. Projekteigene Module erfüllen Referenzen, ohne als +Marketplace-Pakete ausgegeben zu werden. Standard ist eine Vorschau; `--apply` +verlangt einen vollständig sicheren Graphen und installiert Blätter zuerst mit +Rollback. `--apply-resolved` ist die explizite Freigabe für den verifizierten +Teilgraphen und meldet ungelöste Identitäten weiterhin als blockiert. + +Am 4. August 2026 importierte die authentifizierte Abnahme Kafka 2.12.0 +(Content ID 105878) und löste den offiziellen Graphen unter 10.24.0.73019 und +11.12.1. Beide Graphen importierten DataWidgets 3.11.3 mit Content ID 116540 +und Version ID `e7b6d703-8e47-42f4-bb92-934e3601e71b`. Die abschließend +authentifizierte Combo box ist die unabhängige offizielle Widget/clientModule- +Komponente 219304, Version 2.9.0, Version ID +`dce845f4-d051-4161-847c-016c01703caa`. Ihre Installation sichert und ersetzt +das ältere Combo-2.6.x-Asset aus Atlas Core (Content ID 117187); Atlas ist der +frühere Asset-Eigentümer, nicht die Combo-Komponente. Der 11.12-Graph enthält +außerdem Library Logging 1.13.0, Encryption 11.1.1, Mx Model Reflection 9.1.0 +und Mendix Feedback Module 5.0.0 (Content ID 205506). Da der Namensindex +`FeedbackModule` nicht liefert, nutzt MXRB die offizielle Content ID nur als +Discovery-Hinweis und prüft weiterhin den internen MPK-Modulnamen. + +Beide Generationen bestehen jetzt das vollständige Frontend-Abnahme-Gate mit +null Preflight-Befunden für Quelle und Rebuild, struktureller Gleichwertigkeit, +exakten Assets und Marketplace-Provenienz sowie null MxBuild-Fehlern. Ruby- +Export/Rebuild bewahrt den Marketplace-Lock, gecachte MPKs, Originale, +deklarierte Assets und ihre Prüfsummen; das neu aufgebaute Projekt behält damit +die Paketprovenienz und nicht nur die Widget-Bytes. MxBuild bleibt ausschließlich +externe Validierung und nie eine Implementierungsabhängigkeit von MXRB. + +Offizielle Befehle `update` und `remove` sind ohne explizites `--apply` sichere +Vorschauen. Vor einer Änderung vergleicht MXRB Modul und Unit-Anzahl aus dem +Lock mit dem Cache-Paket, durchsucht alle externen Units nach Referenzen auf +wegfallende IDs und verweigert veränderte oder fehlende Paket-Assets. Ein +Update muss die Modul-ID und jede extern referenzierte Unit-ID erhalten. Beim +Anwenden sichert MXRB MPR, v2-`mprcontents`, Lock, Caches, deklarierte Assets und +die Atlas-Variablendatei; jeder Fehler stellt diese gesamte Grenze wieder her. +Gemeinsame Assets werden nicht entfernt, solange ein anderes gesperrtes Paket +sie beansprucht. + ## Protokoll-Connector-Audit `mxrb protocols` ist ein schreibgeschütztes Audit der IoT-, Industrie- und @@ -92,3 +188,27 @@ wird. Diese Protokoll-Registry ist ein dritter Katalog, getrennt von den wiederverwendbaren Ruby-Modulen aus `mxrb module` und den offiziellen Paketen aus `mxrb marketplace`. + +Verifizierte öffentliche Komponenten-IDs decken derzeit MQTT, OPC-UA, Kafka, +AMQP und WebSocket ab. Authentifizierte Abfragen der Content API und des +offiziellen Katalogs liefern weiterhin keine Modbus-Komponente; Modbus bleibt +daher unregistriert, bis eine offizielle Komponente oder ein echtes MPR-Fixture +die Identität belegt. Öffentliche IDs dienen der Planung; die Erkennung im MPR +erfordert weiterhin eine verifizierte `AppStoreGuid`. + +Der Ruby-Builder kann eine Installation deklarieren, ohne ein leeres Modul oder +eine erfundene GUID zu schreiben: + +```ruby +builder.connector :kafka, version: "2.12.0" +plaene = builder.connector_plans(adapter: Mxrb::Protocols.adapter(installer:, api:)) +plaene.each { puts _1.changes } +plaene.each(&:apply!) # explizite authentifizierte Marketplace-Operation +``` + +Ohne Adapter bleibt `connector_plans` eine Vorschau. `build!` schlägt bei noch +offenen Connector-Deklarationen ebenfalls geschlossen fehl; der echte Inhalt +muss zuerst über den offiziellen Adapter in ein MPR installiert werden. +Marketplace-Einträge vom Typ `Module` und `Service` können aufgelöst werden, +während das heruntergeladene MPK zusätzlich die Modul-Paketprüfungen bestehen +muss. diff --git a/docs/de-DE/ruby-first-roadmap.md b/docs/de-DE/ruby-first-roadmap.md index bdaf911..ba16734 100644 --- a/docs/de-DE/ruby-first-roadmap.md +++ b/docs/de-DE/ruby-first-roadmap.md @@ -20,7 +20,8 @@ und MxBuild sind externe Validatoren, keine Abhängigkeiten des Ruby-Kerns. - Statische Analyse und ausführbare Modellbewertungen. - Funktionale Microflow-Tests ohne JUnit. - Lokale oder Docker-Ausführung von `mx check`, MxBuild und Runtime. -- Striktes Gate mit 100 % Zeilen- und Branch-Abdeckung. +- Natives Coverage-Gate mit 100 % Zeilen und 100 % Branches im CI; + ohne explizite Grenzwerte bleibt der lokale Standard strenger bei 100/100. ## Beispiel @@ -84,3 +85,32 @@ Lokale MPKs werden vollständig und ohne Mendix-Werkzeuge direkt über Ruby/SQLite/BSON in das Ziel-MPR importiert. Das PAT benötigt `mx:marketplace-content:read`. Verwundbare Releases werden standardmäßig abgelehnt; Content-/Version-IDs und Sicherheitsdaten stehen im Lockfile. +Die authentifizierte Kafka-Abnahme und der transaktionale Lifecycle sind jetzt +umgesetzt. `marketplace update` und `marketplace remove` zeigen standardmäßig +nur eine Vorschau, erhalten extern referenzierte IDs, verweigern veränderte +Assets und sichern MPR, `mprcontents`, Cache, Lock und Assets vor explizitem +`--apply`. Auch die authentifizierten Folgeschritte sind umgesetzt: +`marketplace dependencies` löst offizielle Pakete rekursiv aus Referenzen im +eingebetteten MPR, prüft die tatsächliche Modulidentität jedes MPKs, erkennt +projekteigene Module, installiert Blätter zuerst und führt atomaren Rollback +aus. Kafka-Graphen bestanden die Abnahme unter 10.24 und 11.12; Ruby- +Export/Rebuild bewahrt Assets, Prüfsummen und Quell-/Rebuild-Diagnosen. + +Die authentifizierte Matrix enthält jetzt DataWidgets 3.11.3 (Content ID +116540, Version ID `e7b6d703-8e47-42f4-bb92-934e3601e71b`) und die unabhängige +offizielle Combo-box-Widget/clientModule-Komponente 219304, Version 2.9.0, +Version ID `dce845f4-d051-4161-847c-016c01703caa`. Die Installation sichert und +ersetzt das zuvor Atlas Core (Content ID 117187) gehörende 2.6.x-Asset. Ruby- +Roundtrips bewahren die Provenienz von Marketplace-Lock, Cache und Originalen; +`script/frontend_acceptance` blockiert Modell-, Asset-, Prüfsummen- oder +Provenienzdrift. Der native Renderer hat für die akzeptierten 10.24- und +11.12-Fixtures null Preflight-Befunde in Quelle und Rebuild. + +Die Migrationsschnittstelle ist als `mxrb frontend migrate DATEI.mpr` +umgesetzt: Standard ist die Vorschau, erst `--apply` schreibt einen sicheren +transaktionalen Plan. Diese Migration ist auf der gesamten unterstützten +Frontend-Matrix abgeschlossen. Das optionale externe MxBuild-Orakel liefert +für Quelle und Rebuild unter 10.24 und 11.12 null Fehler; `mx check` bewahrt +außerdem bytegleiche beobachtbare Paketdiagnosen in jedem Round-trip. MXRB +bleibt unabhängig: `mx` und MxBuild sind nur Validierungsorakel, niemals +Generatoren, Mutatoren oder Runtime-Abhängigkeiten. diff --git a/docs/en-US/architectural-patterns.md b/docs/en-US/architectural-patterns.md index 70a7b77..006707b 100644 --- a/docs/en-US/architectural-patterns.md +++ b/docs/en-US/architectural-patterns.md @@ -144,7 +144,8 @@ back a guess would corrupt the project. ## 10. Coverage as a gate, not a metric -The suite enforces 100% line and branch coverage through -`MXRB_COVERAGE=1 bundle exec rspec`, using Ruby's native `Coverage` API. +CI enforces 100% line and 100% branch coverage through Ruby's native +`Coverage` API. The local helper defaults to 100/100 when no threshold +variables are supplied. The gate measures every executable library line and branch; comments such as `:nocov:` do not remove code from the denominator. See [conventions](conventions.md). diff --git a/docs/en-US/compiler.md b/docs/en-US/compiler.md index 83be84e..f336108 100644 --- a/docs/en-US/compiler.md +++ b/docs/en-US/compiler.md @@ -13,8 +13,8 @@ settings, microflows, and the project/module index. It also creates metadata, dependency files, and a Runtime-ordered BSON `model.mdp`. Bootstrap has audited seeds and catalogs for 6.10.8, 7.5.0, 7.17.0, -9.6.1.29396, and 11.12.1. The 6.x, 7.x, 9.x, and 11.x families select a -compatible compiler seed; 5.x, 8.x, and 10.x fail closed without an audited +9.6.1.29396, 10.24.0.73019, and 11.12.1. The 6.x, 7.x, 9.x, 10.x, and 11.x families select a +compatible compiler seed; 5.x and 8.x fail closed without an audited seed. Family selection applies to compiler schema only. Runtime must always match the MPR's exact patch. @@ -23,9 +23,10 @@ asdf, or mise, compiles `javasource/**/*.java` against Runtime and project libraries, and writes a deterministic OSGi `project.jar`. VetClinic compiled 183 sources into 249 classes. -`WebBundleBuilder` selects Dojo for 6/7, Dojo plus the React wrapper for 9, and -React for 11. The React path generates entrypoint/page modules, expands `.mpk`, -and invokes the version-owned Node/Rspack directly. Data Grid 2 compiles for the XPath datasource and attribute-column +`WebBundleBuilder` selects Dojo for 6/7, Dojo plus the React wrapper for 9, +the project-declared classic or optimized client for 10, and React for 11. +The React path generates entrypoint/page modules, expands `.mpk`, and invokes +the version-owned Node/Rollup (10) or Node/Rspack (11) directly. Data Grid 2 compiles for the XPath datasource and attribute-column subset, including `operations.json`, datasource, and attribute types. Untranslated types or property combinations receive a DOM fallback and are recorded in `web/mxrb-pages.json`; VetClinic's manifest is empty. @@ -140,13 +141,14 @@ cleanly. The `ACT Create Animal` functional test then passed. ## Explicit boundaries - all 12 stages and clean web generation are validated on 6.10.8, 7.5.0, - 7.17.0, 9.6.1.29396, and 11.12.1; -- exact native boot is proven on 11.12.1. The installed 6/7/9 distributions - contain the exact Runtime bundles but no PAD/launcher; `db up` fails closed + 7.17.0, 9.6.1.29396, 10.24.0.73019, and 11.12.1; +- exact native boot is proven on 10.24.0.73019 and 11.12.1. The installed 6/7/9 + distributions contain the exact Runtime bundles but no compatible launcher; `db up` fails closed instead of substituting the Java-21 launcher from 11; - Data Grid 1 is covered while other Dojo widgets remain explicit manifest findings; -- the Mendix 11 React path is covered by clean preflights and complete Rspack +- the optimized Mendix 10 React/Rollup path is covered by a from-scratch app; + the Mendix 11 React path is covered by clean preflights and complete Rspack builds of LearnNow, MyFirstModule, SLATaskApp, Sudoku and VetClinic. This fixture matrix is evidence for the audited contracts, not a universal claim; - native web bundles are generated; React Native still relies on existing diff --git a/docs/en-US/conventions.md b/docs/en-US/conventions.md index 2fa30b2..465ba81 100644 --- a/docs/en-US/conventions.md +++ b/docs/en-US/conventions.md @@ -37,9 +37,10 @@ review. Writing-style guidance for exported projects lives in ## Testing - `bundle exec rspec` is the default suite; everything must pass. -- `MXRB_COVERAGE=1 bundle exec rspec` is the strict gate and requires 100% - line coverage; `bundle exec ruby script/branch_report.rb` lists uncovered - branches by file and line. +- CI sets `MXRB_LINE_COVERAGE_MIN=100` and `MXRB_BRANCH_COVERAGE_MIN=100`; the + local helper defaults to 100/100 when those variables are omitted. + `bundle exec ruby script/branch_report.rb` lists uncovered branches by file + and line. - Coverage uses Ruby's native `Coverage` API, not an external gem. Defensive branches that only fire on corrupt or malformed input — states the writer can never produce — are excluded with `# :nocov:` markers, inline or as diff --git a/docs/en-US/native-runtime-quality-report.md b/docs/en-US/native-runtime-quality-report.md index edf6f89..5f34599 100644 --- a/docs/en-US/native-runtime-quality-report.md +++ b/docs/en-US/native-runtime-quality-report.md @@ -1,18 +1,19 @@ # Native build and Runtime quality report -Date: August 1, 2026. +Date: August 4, 2026. ## Confirmed result - no functional stage invokes `mx`, `mxbuild`, `mxcli`, Studio Pro, or Model SDK; -- clean 12-stage build and web generation passed on 6.10.8 (39 pages), 7.5.0 (45), 7.17.0 (66), 9.6.1.29396 (90), and 11.12.1; +- clean 12-stage build and web generation passed on 6.10.8 (39 pages), 7.5.0 (45), 7.17.0 (66), 9.6.1.29396 (90), 10.24.0.73019 (optimized client), and 11.12.1; - format round-trip matrix passed on six real fixtures from 5.21.4 through 11.12.1: 1,506 units, 1,734 artifacts, and 3,388 references; -- audited compiler schemas/seeds cover families 6.x, 7.x, 9.x, and 11.x. Families 5.x, 8.x, and 10.x fail closed for native compilation; Runtime requires the exact patch; +- audited compiler schemas/seeds cover families 6.x, 7.x, 9.x, 10.x, and 11.x. Families 5.x and 8.x fail closed for native compilation; Runtime requires the exact patch; - Data Grid 1 covers database/XPath/microflow sources, search, sorting, paging, selection, and audited buttons. Data Grid 2 covers XPath, attribute columns, and create action; -- web profiles are Dojo on 6/7, hybrid Dojo/React on 9, and React on 11; +- web profiles are Dojo on 6/7, hybrid Dojo/React on 9, project-selected classic/optimized on 10, and React on 11; - Projects API inventoried 130 accessible apps. Three real Git projects (`MyFirstModule`, `LearnNow Trainning Management`, and `SLATaskApp`) passed validate → export → generate → validate → compare; - `MyFirstModule` was regenerated as exact 11.12.1 without proprietary builders. Runtime synchronized 655 database operations, created the active `mx` administrator, served the React shell and styled login publicly on `127.0.0.1:18080`, and exposed the expected domain tables; -- final QA: 821 examples, zero failures, 100% lines (12,987/12,987), 100% branches (4,611/4,611), and clean RuboCop across 205 files. +- a from-scratch optimized-client 10.24.0.73019 application passed official `mx check` and `mxbuild`, native model/Java/Rollup/portable packaging, 460 database synchronization operations, readiness probe and HTTP 200; +- final QA: 1,002 examples, zero failures, 100.00% lines (16,917/16,917), 100.00% branches (6,659/6,659), and clean RuboCop across 238 files. ## Corrections from the improvement report @@ -35,8 +36,9 @@ Date: August 1, 2026. and `49.95`, while an empty Teacher Gallery remained structurally present; - nanoflow list/object sources and button actions now compile to the real Mendix client property contracts and function-backed instruction programs. - The audited linear instruction subset is tested; unsupported client nodes - fail closed instead of being mislabeled as microflows; + The audited graph subset includes decisions, error paths, nested nanoflows, + JavaScript actions and server microflow calls; unsupported client nodes fail + closed instead of being mislabeled as microflows; - real browser clicks verified Home → Courses → Add → Save. Parameter-backed DataView/TextBox forms render and persist values; create passes GUIDs through `openForm2`, while commit/rollback authorization is derived from page module @@ -48,10 +50,10 @@ Date: August 1, 2026. 1. Dojo widgets other than Data Grid 1 remain listed in `web/mxrb-legacy-pages.json`; they are not silently reported as rendered. 2. Data Grid 2 coverage is the audited XPath/attribute/create subset. Gallery - covers XPath/microflow and the audited linear nanoflow subset. Branches, - JavaScript actions, microflow calls inside nanoflows, and unsafe parameter - mappings still fail closed. -3. Installed 6/7/9 distributions contain exact Runtime bundles but no compatible PAD/launcher. The available 11 launcher requires Java 21 and cannot safely start 9 on Java 11. Legacy `db up` therefore fails closed; only the exact 11.12.1 boot is claimed. + covers XPath/microflow and the audited nanoflow graph subset. Unsupported + graph shapes, missing action/flow references, unsafe parameter mappings and + untranslated client instructions still fail closed. +3. Installed 6/7/9 distributions contain exact Runtime bundles but no compatible launcher. The available 11 launcher requires Java 21 and cannot safely start 9 on Java 11. Legacy `db up` therefore fails closed; exact boot is claimed for 10.24.0.73019 and 11.12.1 only. 4. Runtime uses a local trial/developer license, which emits the expected time-limit warning. 5. Cloud Build/Deploy/Pipelines/Backups are optional external verification adapters, never dependencies of native generation or Runtime startup. diff --git a/docs/en-US/platform-operations.md b/docs/en-US/platform-operations.md index 01a1115..746074e 100644 --- a/docs/en-US/platform-operations.md +++ b/docs/en-US/platform-operations.md @@ -17,6 +17,53 @@ mxrb migrate check . previews unless `--apply` is passed. Migration generates into a temporary area and compares that result with the current MPR; `check` fails on model drift. +## Frontend migration and acceptance + +```sh +mxrb frontend migrate App.mpr --json +mxrb frontend migrate App.mpr --apply --json +script/frontend_acceptance App.mpr -o frontend-round-trip.json +script/frontend_acceptance App.mpr --mxbuild /path/to/mxbuild -o frontend.json +script/frontend_acceptance App.mpr --mx /path/to/mx -o frontend-diagnostics.json +``` + +`mxrb frontend migrate` is an immutable, fail-closed preview by default. For +Mendix 10 and 11 it plans installed pluggable-widget schema updates, legacy +layout-row weights, and design-property normalization from the package XML. +`--apply` writes only a safe plan in one MPR transaction; unknown configured +properties, an unsupported generation, or a unit changed after preview block +the write. Neither preview nor apply invokes Mendix tooling. + +`script/frontend_acceptance` is the reproducible 10/11 gate. It validates the +source and rebuilt MPRs, requires a compatible native preflight on both, +exports to Ruby and rebuilds, compares model structure, and verifies complete +asset inventories, bytes, SHA-256 checksums, and Marketplace provenance. The +provenance boundary includes `.mxrb/marketplace.lock.json`, the package cache, +and `.mxrb/marketplace-originals`; missing, changed, or unexpected files fail +the gate. The accepted renderer matrix now covers Forms tables, +`ListViewXPathSource`, listen-target object properties, structured page-variable +mappings, and Combo-box enumerations. Its preflight baseline fell from 28 to 0 +findings on 10.24 and from 20 to 0 on 11.12. + +Without `--mxbuild`, the report scope is `round_trip` and `frontend_ready` +remains unset. With `--mxbuild`, MxBuild is a read-only external oracle and the +scope becomes `frontend`; it never generates, mutates, or repairs the project. +The live oracle accepts success only when MxBuild exits zero and produces a +non-empty MDA; a nonzero toolchain exit without reported model errors fails +closed instead of being certified as a clean model. +On August 4, 2026, the safe migration completed the accepted 10.24.0.73019 and +11.12.1 matrix: source and rebuilt projects returned zero MxBuild errors and +both reports set `frontend_ready` to `true`. + +`--mx` runs the official read-only checker with warnings, deprecations, and +best-practice recommendations enabled. It validates the checker's exit bitmask +and compares normalized source/rebuilt signatures. The accepted 10.24 fixture +has 0 errors, 173 package-owned warnings, 0 deprecations, and 2 Kafka +recommendations; 11.12 has 0 errors, 10 package-owned warnings, 0 deprecations, +and the same 2 recommendations. Source and rebuilt JSON are byte-identical in +both generations, so these observable package diagnostics do not represent +MXRB round-trip drift. + ## Official and community Mendix Marketplace MXRB uses the documented Mendix Marketplace Content API. Create a PAT with the @@ -31,6 +78,13 @@ mxrb marketplace show 170 mxrb marketplace versions 170 --mendix-version 11.12.1 mxrb marketplace pull 170 --mpr MyApp.mpr mxrb marketplace pull 170@3.4.0 --mpr MyApp.mpr +mxrb marketplace dependencies CommunityCommons --mpr MyApp.mpr +mxrb marketplace dependencies CommunityCommons --mpr MyApp.mpr --apply +mxrb marketplace dependencies CommunityCommons --mpr MyApp.mpr --apply-resolved +mxrb marketplace update 170@3.5.0 --mpr MyApp.mpr +mxrb marketplace update 170@3.5.0 --mpr MyApp.mpr --apply +mxrb marketplace remove CommunityCommons --mpr MyApp.mpr +mxrb marketplace remove CommunityCommons --mpr MyApp.mpr --apply mxrb marketplace pull github:mendix/CommunityCommons mxrb marketplace import ./CommunityCommons.mpk --mpr MyApp.mpr mxrb marketplace audit --target . --mendix-version 11.12.1 @@ -78,6 +132,46 @@ The PAT is sent only to the exact official hosts `marketplace-api.mendix.com` and `marketplace.mendix.com`; redirects to any other host do not receive it. See the [Mendix Marketplace Content API](https://docs.mendix.com/apidocs-mxsdk/apidocs/content-api/). +`marketplace dependencies` discovers unresolved qualified module references in +the embedded package MPR, resolves them recursively through the official API, +downloads each candidate, and accepts it only when the MPK itself proves the +requested module identity. Host-owned project modules satisfy references +without being mislabeled as Marketplace packages. The command previews by +default; `--apply` requires a complete safe graph and installs leaf-first with +rollback, while `--apply-resolved` is the explicit opt-in for a verified partial +graph and still returns a blocked status for unresolved identities. + +Authenticated acceptance on August 4, 2026 imported Kafka 2.12.0 (Content ID +105878) and resolved its official dependency graph on both 10.24.0.73019 and +11.12.1. Both graphs imported DataWidgets 3.11.3 with Content ID 116540 and +Version ID `e7b6d703-8e47-42f4-bb92-934e3601e71b`. The final authenticated +Combo box is the independent official Widget/clientModule component 219304, +version 2.9.0, Version ID `dce845f4-d051-4161-847c-016c01703caa`. Its install +backs up and replaces the older 2.6.x Combo asset brought by Atlas Core +(Content ID 117187); Atlas is the prior asset owner, not the Combo component. +The 11.12 graph also includes Library Logging 1.13.0, Encryption 11.1.1, +Mx Model Reflection 9.1.0, and Mendix Feedback Module 5.0.0 (Content ID +205506). The API name index does not expose `FeedbackModule`, so MXRB uses the +official component ID only as a discovery hint and still verifies the +downloaded MPK name before accepting it. + +Both generations now pass the complete frontend acceptance gate with zero +source and rebuilt preflight findings, structural equivalence, exact asset and +Marketplace provenance, and zero MxBuild errors. The Ruby export/rebuild +preserves the Marketplace lock, cached MPKs, originals, declared assets, and +their checksums, so the rebuilt project retains package provenance instead of +merely retaining widget bytes. MxBuild remains external validation only, never +an MXRB implementation dependency. + +Official `update` and `remove` are safe previews unless `--apply` is explicit. +Before mutation, MXRB checks the locked module and unit count against the cached +package, scans every unit outside the module for references to IDs that would +disappear, and refuses changed or missing package assets. An update must retain +the module ID and any externally referenced unit IDs. Apply snapshots the MPR, +v2 `mprcontents`, lock, caches, declared assets, and Atlas variables file; any +failure restores that complete boundary. Shared assets are never removed while +another locked package claims them. + ## Protocol connector audit `mxrb protocols` is a read-only audit of the IoT, industrial, and messaging @@ -97,3 +191,25 @@ unrecognized rather than guessed. This protocol registry is a third catalog, distinct from the reusable Ruby modules of `mxrb module` and the official packages of `mxrb marketplace`. + +Verified public component ids currently cover MQTT, OPC-UA, Kafka, AMQP and +WebSocket. Authenticated Content API queries and the official catalog search +still provide no Modbus component, so it remains unregistered until an official +component or real MPR fixture proves its identity. Public ids support planning; +recognition inside an MPR still requires a verified `AppStoreGuid`. + +The Ruby builder can declare an installation request without writing an empty +module or invented GUID: + +```ruby +builder.connector :kafka, version: "2.12.0" +plans = builder.connector_plans(adapter: Mxrb::Protocols.adapter(installer:, api:)) +plans.each { puts _1.changes } +plans.each(&:apply!) # explicit authenticated Marketplace operation +``` + +`connector_plans` is preview-only without the adapter. A regular `build!` also +fails closed while connector declarations are pending; connector content must +be installed into a real MPR through the official Marketplace adapter first. +Marketplace `Module` and `Service` entries may be resolved, while the downloaded +MPK independently has to pass the module-package boundary checks. diff --git a/docs/en-US/ruby-first-roadmap.md b/docs/en-US/ruby-first-roadmap.md index e25d277..eca4c6b 100644 --- a/docs/en-US/ruby-first-roadmap.md +++ b/docs/en-US/ruby-first-roadmap.md @@ -28,7 +28,8 @@ Ruby is MXRB's only public language. - Executable Ruby model evaluations with severity and scores. - Functional microflow tests without JUnit. - Local or Docker execution of `mx check`, portable MxBuild and Runtime. -- A strict 100% library line-and-branch coverage gate. +- A native coverage gate: 100% lines and 100% branches in CI, with a + stricter 100/100 local default when thresholds are omitted. ## Semantic API @@ -155,3 +156,31 @@ including the complete unit tree and declared assets, without Mendix tools. The PAT requires `mx:marketplace-content:read`. Official downloads are selected against the target MPR version, vulnerable releases are denied by default, and Content/Version IDs plus security metadata are written to the lockfile. +Authenticated Kafka acceptance and transactional lifecycle are now delivered. +`marketplace update` and `marketplace remove` preview by default, preserve +externally referenced IDs, reject changed assets, and snapshot MPR, +`mprcontents`, cache, lock, and assets before explicit `--apply`. Remaining +authenticated work is also delivered: `marketplace dependencies` recursively +resolves official packages from references in the embedded MPR, verifies each +downloaded MPK's actual module identity, recognizes host-owned modules, applies +leaf-first, and rolls back atomically. Kafka package graphs passed authenticated +10.24 and 11.12 acceptance, and imported modules now export to Ruby and rebuild +with asset checksums and source/rebuild diagnostics preserved. + +The authenticated matrix now includes DataWidgets 3.11.3 (Content ID 116540, +Version ID `e7b6d703-8e47-42f4-bb92-934e3601e71b`) and the independent official +Combo box Widget/clientModule component 219304, version 2.9.0, Version ID +`dce845f4-d051-4161-847c-016c01703caa`. Combo installation backs up and +replaces the 2.6.x asset previously owned by Atlas Core (Content ID 117187). +Ruby round trips preserve Marketplace lock/cache/original provenance, and +`script/frontend_acceptance` fails on model, asset, checksum, or provenance +drift. Native renderer coverage for the accepted 10.24 and 11.12 fixtures has +zero source and rebuilt preflight findings. + +The migration interface is delivered as `mxrb frontend migrate FILE.mpr`: it +previews by default and writes a safe transactional plan only with `--apply`. +That migration track is complete across the supported frontend matrix. The +optional external MxBuild oracle returns zero errors for source and rebuild on +10.24 and 11.12; `mx check` also preserves byte-identical observable package +diagnostics across each round trip. MXRB remains independent: `mx` and MxBuild +are validation oracles only, never generators, mutators, or runtime dependencies. diff --git a/docs/en-US/writing.md b/docs/en-US/writing.md index 051cd5d..0b056eb 100644 --- a/docs/en-US/writing.md +++ b/docs/en-US/writing.md @@ -228,10 +228,11 @@ The default suite runs with `bundle exec rspec`. The strict coverage gate uses Ruby's native `Coverage` API: ```sh -MXRB_COVERAGE=1 bundle exec rspec +MXRB_COVERAGE=1 MXRB_LINE_COVERAGE_MIN=100 MXRB_BRANCH_COVERAGE_MIN=100 bundle exec rspec ``` -It writes `coverage/coverage.json` and requires 100% line and branch coverage. +It writes `coverage/coverage.json`; CI requires 100% lines and 100% branches, +while the helper defaults to 100/100 if no thresholds are supplied. `bundle exec ruby script/branch_report.rb` lists an uncovered branch by source file and line. `bundle exec rubocop` is the gradual static-quality gate. diff --git a/docs/pt-BR/compiler.md b/docs/pt-BR/compiler.md index c4d3dd5..aa533d2 100644 --- a/docs/pt-BR/compiler.md +++ b/docs/pt-BR/compiler.md @@ -15,8 +15,8 @@ gera `metadata.json`, dependências e o `model.mdp` em ordem BSON aceita pelo Runtime. O bootstrap possui seeds e catálogos auditados para 6.10.8, 7.5.0, 7.17.0, -9.6.1.29396 e 11.12.1. As famílias 6.x, 7.x, 9.x e 11.x selecionam o seed -compatível; 5.x, 8.x e 10.x falham fechadas por ainda não terem seed auditado. +9.6.1.29396, 10.24.0.73019 e 11.12.1. As famílias 6.x, 7.x, 9.x, 10.x e 11.x selecionam o seed +compatível; 5.x e 8.x falham fechadas por ainda não terem seed auditado. Esse agrupamento vale para o schema do compilador. A Runtime deve sempre ter o mesmo patch exato do MPR. @@ -25,9 +25,10 @@ mise, compila `javasource/**/*.java` com as bibliotecas do Runtime e do projeto e escreve um `project.jar` OSGi determinístico. No VetClinic foram compilados 183 fontes em 249 classes. -`WebBundleBuilder` seleciona Dojo em 6/7, Dojo com React wrapper em 9 e React -em 11. No React, gera entrypoint e módulos, expande `.mpk` e chama diretamente -o Node/Rspack da versão. O Data Grid 2 é compilado no subconjunto com +`WebBundleBuilder` seleciona Dojo em 6/7, Dojo com React wrapper em 9, o cliente +clássico ou otimizado declarado pelo projeto em 10 e React em 11. No React, +gera entrypoint e módulos, expande `.mpk` e chama diretamente o Node/Rollup +da versão 10 ou Node/Rspack da versão 11. O Data Grid 2 é compilado no subconjunto com fonte XPath e colunas de atributo, incluindo `operations.json`, datasource e tipos dos atributos. Tipos ou combinações ainda não traduzidos recebem um fallback DOM e ficam registrados em `web/mxrb-pages.json`; o manifesto do @@ -145,13 +146,14 @@ encerrou limpo. O teste funcional `ACT Create Animal` passou em seguida. ## Limites explícitos - os 12 estágios e a geração web limpa foram validados em 6.10.8, 7.5.0, - 7.17.0, 9.6.1.29396 e 11.12.1; -- o boot nativo exato foi provado em 11.12.1. As distribuições 6/7/9 instaladas - têm os bundles exatos, mas não PAD/launcher; `db up` falha fechado em vez de + 7.17.0, 9.6.1.29396, 10.24.0.73019 e 11.12.1; +- o boot nativo exato foi provado em 10.24.0.73019 e 11.12.1. As distribuições + 6/7/9 instaladas têm os bundles exatos, mas não launcher compatível; `db up` falha fechado em vez de substituir pelo launcher Java 21 da versão 11; - Data Grid 1 está coberto, mas os demais widgets Dojo continuam explicitamente pendentes no manifesto legado; -- o caminho React do Mendix 11 tem preflight limpo e builds Rspack completos de +- o caminho React/Rollup otimizado do Mendix 10 está coberto por uma aplicação + criada do zero; o caminho React do Mendix 11 tem preflight limpo e builds Rspack completos de LearnNow, MyFirstModule, SLATaskApp, Sudoku e VetClinic. Essa matriz comprova os contratos auditados, não compatibilidade universal; - bundles web nativos são gerados; o pipeline React Native ainda depende dos diff --git a/docs/pt-BR/native-runtime-quality-report.md b/docs/pt-BR/native-runtime-quality-report.md index 778d197..01bc80a 100644 --- a/docs/pt-BR/native-runtime-quality-report.md +++ b/docs/pt-BR/native-runtime-quality-report.md @@ -1,18 +1,19 @@ # Relatório de qualidade do build e Runtime nativos -Data: 1º de agosto de 2026. +Data: 4 de agosto de 2026. ## Resultado confirmado - nenhuma etapa funcional chama `mx`, `mxbuild`, `mxcli`, Studio Pro ou Model SDK; -- build limpo de 12 estágios e geração web passaram em 6.10.8 (39 páginas), 7.5.0 (45), 7.17.0 (66), 9.6.1.29396 (90) e 11.12.1; +- build limpo de 12 estágios e geração web passaram em 6.10.8 (39 páginas), 7.5.0 (45), 7.17.0 (66), 9.6.1.29396 (90), 10.24.0.73019 (cliente otimizado) e 11.12.1; - a matriz de round-trip passou em seis fixtures reais de 5.21.4 a 11.12.1: 1.506 units, 1.734 artefatos e 3.388 referências; -- schemas/seeds auditados do compilador cobrem 6.x, 7.x, 9.x e 11.x. 5.x, 8.x e 10.x falham fechadas na compilação nativa; a Runtime exige o patch exato; +- schemas/seeds auditados do compilador cobrem 6.x, 7.x, 9.x, 10.x e 11.x. 5.x e 8.x falham fechadas na compilação nativa; a Runtime exige o patch exato; - Data Grid 1 cobre database/XPath/microflow, pesquisa, ordenação, paginação, seleção e botões auditados. Data Grid 2 cobre XPath, colunas de atributo e ação create; -- perfis web: Dojo em 6/7, híbrido Dojo/React em 9 e React em 11; +- perfis web: Dojo em 6/7, híbrido Dojo/React em 9, clássico/otimizado conforme o projeto em 10 e React em 11; - a Projects API inventariou 130 apps. Três projetos Git reais (`MyFirstModule`, `LearnNow Trainning Management` e `SLATaskApp`) passaram validate → export → generate → validate → compare; - `MyFirstModule` foi regenerado como 11.12.1 exato sem builders proprietários. A Runtime sincronizou 655 operações, criou o administrador `mx` ativo, serviu shell React e login estilizado em `127.0.0.1:18080` e expôs as tabelas esperadas; -- QA final: 821 exemplos, zero falhas, 100% de linhas (12.987/12.987), 100% de branches (4.611/4.611) e RuboCop limpo em 205 arquivos. +- uma aplicação 10.24.0.73019 com cliente otimizado, criada do zero, passou no `mx check` e `mxbuild` oficiais, modelo/Java/Rollup/pacote portátil nativos, 460 operações de sincronização, probe de prontidão e HTTP 200; +- QA final: 1.002 exemplos, zero falhas, 100,00% de linhas (16.917/16.917), 100,00% de branches (6.659/6.659) e RuboCop limpo em 238 arquivos. ## Correções do relatório de melhorias @@ -35,8 +36,9 @@ Data: 1º de agosto de 2026. permaneceu presente na estrutura; - fontes nanoflow de lista/objeto e ações de botão agora compilam para os contratos reais do cliente Mendix e programas de instruções referenciados por - função. O subconjunto linear auditado é testado; nós não suportados falham - fechado em vez de serem tratados incorretamente como microflows; + função. O subconjunto auditado do grafo inclui decisões, caminhos de erro, + nanoflows aninhados, ações JavaScript e chamadas de microflow servidor; nós + não suportados falham fechado em vez de serem tratados como microflows; - cliques reais no navegador validaram Home → Courses → Add → Save. Formulários DataView/TextBox por parâmetro renderizam e persistem valores; create passa o GUID por `openForm2`, enquanto a autorização de commit/rollback deriva dos @@ -48,10 +50,10 @@ Data: 1º de agosto de 2026. 1. Widgets Dojo além do Data Grid 1 permanecem listados em `web/mxrb-legacy-pages.json`; não são declarados renderizados. 2. Data Grid 2 cobre o subconjunto auditado XPath/atributo/create. Gallery cobre - XPath/microflow e o subconjunto linear auditado de nanoflow. Decisões, ações - JavaScript, chamadas de microflow internas e mapeamentos inseguros ainda - falham fechado. -3. As distribuições 6/7/9 instaladas têm bundles exatos, mas não PAD/launcher compatível. O launcher 11 exige Java 21 e não inicia 9 com segurança no Java 11. O `db up` legado falha fechado; só o boot 11.12.1 exato é afirmado. + XPath/microflow e o subconjunto auditado do grafo nanoflow. Formatos de grafo + não suportados, referências ausentes, mapeamentos de parâmetros inseguros e + instruções cliente não traduzidas ainda falham fechado. +3. As distribuições 6/7/9 instaladas têm bundles exatos, mas não launcher compatível. O launcher 11 exige Java 21 e não inicia 9 com segurança no Java 11. O `db up` legado falha fechado; boot exato é afirmado somente para 10.24.0.73019 e 11.12.1. 4. A Runtime usa licença local trial/developer e emite o aviso esperado de limite de tempo. 5. Build/Deploy/Pipelines/Backups em nuvem são verificações externas opcionais, nunca dependências do build/runtime nativo. diff --git a/docs/pt-BR/platform-operations.md b/docs/pt-BR/platform-operations.md index 8b1d427..ec20e19 100644 --- a/docs/pt-BR/platform-operations.md +++ b/docs/pt-BR/platform-operations.md @@ -19,6 +19,53 @@ mxrb migrate check . `project.rb`. Migration gera o projeto em área temporária e compara o modelo resultante com o MPR atual; `check` retorna erro quando existe drift. +## Migração e aceitação do frontend + +```sh +mxrb frontend migrate App.mpr --json +mxrb frontend migrate App.mpr --apply --json +script/frontend_acceptance App.mpr -o frontend-round-trip.json +script/frontend_acceptance App.mpr --mxbuild /caminho/mxbuild -o frontend.json +script/frontend_acceptance App.mpr --mx /caminho/mx -o frontend-diagnostics.json +``` + +`mxrb frontend migrate` é uma prévia imutável e fail-closed por padrão. Para +Mendix 10 e 11, planeja a atualização dos schemas dos pluggable widgets +instalados, dos pesos legados de linhas de layout e das design properties a +partir do XML do pacote. `--apply` grava somente um plano seguro em uma única +transação no MPR; propriedades configuradas desconhecidas, geração não +suportada ou unit alterada depois da prévia bloqueiam a escrita. Nem a prévia +nem a aplicação executam ferramentas Mendix. + +`script/frontend_acceptance` é o gate reproduzível de 10/11. Ele valida os MPRs +de origem e reconstruído, exige preflight nativo compatível em ambos, exporta +para Ruby e reconstrói, compara a estrutura do modelo e verifica +inventários completos de assets, bytes, SHA-256 e proveniência do Marketplace. +A fronteira de proveniência inclui `.mxrb/marketplace.lock.json`, o cache de +pacotes e `.mxrb/marketplace-originals`; arquivo ausente, alterado ou inesperado +reprova o gate. A matriz aceita do renderer agora cobre tabelas de Forms, +`ListViewXPathSource`, propriedades de objeto de listen target, mappings +estruturados de variáveis de página e enumerações de Combo box. O baseline do +preflight caiu de 28 para 0 achados no 10.24 e de 20 para 0 no 11.12. + +Sem `--mxbuild`, o relatório tem escopo `round_trip` e deixa `frontend_ready` +sem valor. Com `--mxbuild`, o MxBuild atua como oráculo externo somente leitura +e o escopo passa a `frontend`; ele nunca gera, altera ou repara o projeto. +O oráculo ao vivo só aceita sucesso quando o MxBuild termina com status zero e +produz um MDA não vazio; falha não zero da toolchain sem erros de modelo +registrados falha fechada em vez de certificar um modelo limpo. +Em 4 de agosto de 2026, a migração segura concluiu a matriz aceita 10.24.0.73019 e +11.12.1: origem e reconstrução retornaram zero erros no MxBuild e os dois +relatórios marcaram `frontend_ready` como `true`. + +`--mx` executa o checker oficial somente leitura com warnings, depreciações e +recomendações de boas práticas. O gate valida o bitmask de saída e compara as +assinaturas normalizadas de origem e reconstrução. A fixture 10.24 aceita tem +0 erros, 173 warnings dos pacotes, 0 depreciações e 2 recomendações do Kafka; +a 11.12 tem 0 erros, 10 warnings dos pacotes, 0 depreciações e as mesmas 2 +recomendações. Os JSONs de origem e reconstrução são idênticos byte a byte nas +duas gerações; esses diagnósticos observáveis dos pacotes não são drift do MXRB. + ## Marketplace Mendix oficial e comunitário O MXRB usa a Marketplace Content API documentada pela Mendix. Crie um PAT com @@ -32,6 +79,13 @@ mxrb marketplace show 170 mxrb marketplace versions 170 --mendix-version 11.12.1 mxrb marketplace pull 170 --mpr VetClinic.mpr mxrb marketplace pull 170@3.4.0 --mpr VetClinic.mpr +mxrb marketplace dependencies CommunityCommons --mpr VetClinic.mpr +mxrb marketplace dependencies CommunityCommons --mpr VetClinic.mpr --apply +mxrb marketplace dependencies CommunityCommons --mpr VetClinic.mpr --apply-resolved +mxrb marketplace update 170@3.5.0 --mpr VetClinic.mpr +mxrb marketplace update 170@3.5.0 --mpr VetClinic.mpr --apply +mxrb marketplace remove CommunityCommons --mpr VetClinic.mpr +mxrb marketplace remove CommunityCommons --mpr VetClinic.mpr --apply mxrb marketplace pull github:mendix/CommunityCommons mxrb marketplace import ./CommunityCommons.mpk --mpr VetClinic.mpr mxrb marketplace audit --target . --mendix-version 11.12.1 @@ -88,6 +142,48 @@ O token só é enviado aos hosts oficiais exatos redirects para qualquer outro host. Consulte a [Marketplace Content API](https://docs.mendix.com/apidocs-mxsdk/apidocs/content-api/). +`marketplace dependencies` descobre referências qualificadas não resolvidas no +MPR interno do pacote, resolve o grafo recursivamente pela API oficial, baixa +cada candidato e só o aceita quando o próprio MPK comprova a identidade do +módulo solicitado. Módulos pertencentes ao projeto satisfazem referências sem +serem rotulados como pacotes do Marketplace. O padrão é preview; `--apply` +exige grafo completo e seguro, instala folhas primeiro e faz rollback, enquanto +`--apply-resolved` é a opção explícita para aplicar apenas a parte comprovada e +ainda retorna estado bloqueado para identidades não resolvidas. + +Em 4 de agosto de 2026, a aceitação autenticada importou Kafka 2.12.0 (Content +ID 105878) e resolveu o grafo oficial em 10.24.0.73019 e 11.12.1. Os dois grafos +importaram DataWidgets 3.11.3 com Content ID 116540 e Version ID +`e7b6d703-8e47-42f4-bb92-934e3601e71b`. O Combo box autenticado final é o +componente oficial independente Widget/clientModule 219304, versão 2.9.0, +Version ID `dce845f4-d051-4161-847c-016c01703caa`. Sua instalação faz backup e +substitui o asset Combo 2.6.x anterior trazido pelo Atlas Core (Content ID +117187); o Atlas é o proprietário anterior do asset, não o componente Combo. +O grafo 11.12 também inclui Library Logging 1.13.0, Encryption 11.1.1, Mx Model +Reflection 9.1.0 e Mendix Feedback Module 5.0.0 (Content ID 205506). Como o +índice por nome da API não expõe `FeedbackModule`, o MXRB usa o ID oficial +somente como dica de descoberta e ainda exige que o MPK baixado prove o nome +interno. + +As duas gerações agora passam no gate completo de aceitação do frontend com +zero achados no preflight da origem e da reconstrução, equivalência estrutural, +assets e proveniência do Marketplace exatos e zero erros no MxBuild. O +export/rebuild Ruby preserva o lock do Marketplace, MPKs em cache, originais, +assets declarados e seus checksums; assim, o projeto reconstruído mantém a proveniência dos pacotes, +e não apenas os bytes dos widgets. A trilha segura de migração e os oráculos +externos MxBuild/`mx check` estão concluídos na matriz suportada. Essas +ferramentas continuam sendo apenas validação externa, nunca dependências da +implementação MXRB. + +`update` e `remove` oficiais são prévias seguras sem `--apply`. Antes de +alterar o projeto, o MXRB compara módulo e contagem de units do lock com o +pacote em cache, varre todas as units externas em busca de referências a IDs +que desapareceriam e recusa assets alterados ou ausentes. Um update precisa +preservar o ID do módulo e todo ID referenciado externamente. Ao aplicar, o MXRB +protege MPR, `mprcontents` v2, lock, caches, assets declarados e arquivo de +variáveis do Atlas; qualquer falha restaura toda essa fronteira. Assets +compartilhados não são removidos enquanto outro pacote no lock os declarar. + Há dois marketplaces distintos: `mxrb module search/add` instala módulos Ruby reutilizáveis do ecossistema MXRB; `mxrb marketplace ...` instala pacotes Mendix oficiais/comunitários. @@ -112,3 +208,26 @@ relatado como não reconhecido, nunca inferido. Este registro de protocolos é um terceiro catálogo, distinto dos módulos Ruby reutilizáveis de `mxrb module` e dos pacotes oficiais de `mxrb marketplace`. + +Os ids públicos verificados atualmente cobrem MQTT, OPC-UA, Kafka, AMQP e +WebSocket. Consultas autenticadas à Content API e ao catálogo oficial ainda não +retornam componente Modbus; por isso ele continua sem cadastro até que um +componente oficial ou uma fixture MPR real comprove sua identidade. O id +público serve para planejamento; o reconhecimento dentro de um MPR ainda exige +`AppStoreGuid` verificado. + +O builder Ruby pode declarar a intenção de instalação sem escrever módulo vazio +nem GUID inventado: + +```ruby +builder.connector :kafka, version: "2.12.0" +planos = builder.connector_plans(adapter: Mxrb::Protocols.adapter(installer:, api:)) +planos.each { puts _1.changes } +planos.each(&:apply!) # operação autenticada e explícita no Marketplace +``` + +Sem o adapter, `connector_plans` é somente prévia. `build!` também falha fechado +enquanto houver declarações pendentes: o conteúdo real deve ser instalado no +MPR pelo adapter oficial. Entradas `Module` e `Service` podem ser resolvidas, +mas o MPK baixado ainda precisa passar independentemente pelas validações de +fronteira de pacote de módulo. diff --git a/docs/pt-BR/ruby-first-roadmap.md b/docs/pt-BR/ruby-first-roadmap.md index d5aab51..4e3880b 100644 --- a/docs/pt-BR/ruby-first-roadmap.md +++ b/docs/pt-BR/ruby-first-roadmap.md @@ -35,7 +35,8 @@ Ruby é a única linguagem pública do MXRB. descartável e executados pelo runtime Mendix sem JUnit. - Execução local ou Docker de `mx check`, pacote portátil e runtime, com seleção automática da família Java do projeto. -- Gate nativo de 100% de cobertura de linhas e branches para a biblioteca. +- Gate nativo de cobertura: 100% de linhas e 100% de branches no CI, + com default local mais estrito de 100/100 quando os limites são omitidos. Projetos graváveis mantêm um cache do índice semântico identificado por fingerprint. `mxrb cache status`, `warm` e `clear` expõem métricas e manutenção; @@ -179,14 +180,18 @@ Arquivos de avaliação são Ruby comum e rodam com ## Marketplace oficial Mendix -Esta família será separada de `mxrb module`, que instala módulos Ruby do -catálogo interno. O formato previsto é: +Esta família permanece separada de `mxrb module`, que instala módulos Ruby do +catálogo interno. O formato entregue é: ```sh mxrb marketplace search "Community Commons" mxrb marketplace show 170 mxrb marketplace versions 170 --mendix-version 11.12.1 mxrb marketplace pull 170 --mpr App.mpr +mxrb marketplace update 170 --mpr App.mpr +mxrb marketplace update 170 --mpr App.mpr --apply +mxrb marketplace remove CommunityCommons --mpr App.mpr +mxrb marketplace remove CommunityCommons --mpr App.mpr --apply mxrb marketplace pull github:mendix/CommunityCommons mxrb marketplace import ./CommunityCommons.mpk --mpr App.mpr mxrb marketplace login --pat-file .env @@ -204,6 +209,31 @@ Capacidades entregues: no MPR de destino via Ruby/SQLite/BSON, sem ferramentas Mendix. 4. PAT Mendix: validar e armazenar com permissões restritas, exigindo o escopo `mx:marketplace-content:read` e enviando-o somente ao host oficial da API. +5. Lifecycle transacional: `update` e `remove` são prévias por padrão, preservam + IDs referenciados externamente, recusam assets alterados e protegem MPR, + `mprcontents`, cache, lock e assets antes de `--apply` explícito. +6. Dependências oficiais: `marketplace dependencies` lê referências no MPR + interno, resolve recursivamente, confirma a identidade real em cada MPK, + reconhece módulos do próprio projeto, instala folhas primeiro e faz rollback. +7. Export/rebuild autenticado: os grafos Kafka passaram em 10.24 e 11.12, com + assets, checksums e diagnósticos fonte/reconstrução preservados. +8. Frontend oficial: DataWidgets 3.11.3 foi importado com Content ID 116540 e + Version ID `e7b6d703-8e47-42f4-bb92-934e3601e71b`; o Combo box é o componente + oficial independente Widget/clientModule 219304, versão 2.9.0, Version ID + `dce845f4-d051-4161-847c-016c01703caa`. A instalação faz backup e substitui + o asset 2.6.x antes pertencente ao Atlas Core (Content ID 117187). +9. Aceitação reproduzível: `script/frontend_acceptance` bloqueia drift de + modelo, assets, checksums ou proveniência do lock/cache/originais. O renderer + nativo tem zero achados de preflight na origem e reconstrução das fixtures + aceitas de 10.24 e 11.12. +10. Migração segura: `mxrb frontend migrate App.mpr` mostra a prévia e somente + `--apply` grava um plano transacional considerado seguro. GitHub não cobre módulos sem fonte ou release pública; MPK local continua útil como rota offline. A integração oficial usa somente o contrato OpenAPI publicado. +A trilha de migração está concluída em toda a matriz de frontend suportada. O +oráculo MxBuild externo e opcional retorna zero erros na origem e reconstrução +de 10.24 e 11.12; `mx check` também preserva diagnósticos observáveis dos +pacotes byte a byte em cada round-trip. O MXRB permanece independente: `mx` e +MxBuild são somente oráculos de validação, nunca geradores, mutadores ou +dependências de runtime. diff --git a/lib/mxrb.rb b/lib/mxrb.rb index 6a2ad97..845156a 100644 --- a/lib/mxrb.rb +++ b/lib/mxrb.rb @@ -16,6 +16,7 @@ require_relative "mxrb/scaffold/cli" require_relative "mxrb/io/bson_codec" require_relative "mxrb/widget_package" +require_relative "mxrb/frontend/migrator" require_relative "mxrb/io/mxunit_codec" require_relative "mxrb/io/mpr_file" require_relative "mxrb/model/unit" diff --git a/lib/mxrb/compiler/adapter.rb b/lib/mxrb/compiler/adapter.rb index 7210e68..4eb622e 100644 --- a/lib/mxrb/compiler/adapter.rb +++ b/lib/mxrb/compiler/adapter.rb @@ -17,20 +17,21 @@ class Adapter attr_reader :version, :major - def self.for(version) + def self.for(version, source: nil) major = version.to_s.split('.').first.to_i - klass = { 6 => V6, 7 => V7, 9 => V9, 11 => V11 }[major] + klass = { 6 => V6, 7 => V7, 9 => V9, 10 => V10, 11 => V11 }[major] unless klass raise UnsupportedVersion, - "audited native compilation supports Mendix 6.x, 7.x, 9.x, and 11.x; got #{version}" + "audited native compilation supports Mendix 6.x, 7.x, 9.x, 10.x, and 11.x; got #{version}" end - klass.new(version) + klass.new(version, source:) end - def initialize(version) + def initialize(version, source: nil) @version = version.to_s @major = @version.split('.').first.to_i + @source = source end def validate_deployment!(root) @@ -80,6 +81,12 @@ class V9 < Adapter def web_profiles = %i[dojo react_wrapper] end + # Mendix 10 owns both the classic Dojo client and the opt-in optimized + # React client. The selection is stored in the project model. + class V10 < Adapter + def web_profiles = @source&.optimized_web_client? ? [:react] : [:dojo] + end + # Current fully React web client contract. class V11 < Adapter def web_profiles = [:react] diff --git a/lib/mxrb/compiler/artifact_materializer.rb b/lib/mxrb/compiler/artifact_materializer.rb index 429143e..ae19641 100644 --- a/lib/mxrb/compiler/artifact_materializer.rb +++ b/lib/mxrb/compiler/artifact_materializer.rb @@ -30,7 +30,7 @@ def materialize def compile_documents(source) compiler = ArtifactDocumentCompiler.new source.units.select { ArtifactDocumentCompiler::TYPES.include?(_1.document['$Type']) } - .map { compiler.compile(_1) } + .map { compiler.compile(_1) } end def write_model(path, documents) diff --git a/lib/mxrb/compiler/code_action_materializer.rb b/lib/mxrb/compiler/code_action_materializer.rb index 5be1890..f30bb5d 100644 --- a/lib/mxrb/compiler/code_action_materializer.rb +++ b/lib/mxrb/compiler/code_action_materializer.rb @@ -28,7 +28,7 @@ def materialize def compile_documents(source) compiler = CodeActionDocumentCompiler.new source.units.select { CodeActionDocumentCompiler::TYPES.include?(_1.document['$Type']) } - .map { compiler.compile(_1) } + .map { compiler.compile(_1) } end end end diff --git a/lib/mxrb/compiler/combo_box_bundle_compiler.rb b/lib/mxrb/compiler/combo_box_bundle_compiler.rb index 68b5bf1..c83d170 100644 --- a/lib/mxrb/compiler/combo_box_bundle_compiler.rb +++ b/lib/mxrb/compiler/combo_box_bundle_compiler.rb @@ -24,8 +24,7 @@ def initialize(source, page_name, widget, scope:, entity:) end def supported? - widget_type&.fetch('WidgetId', nil) == WIDGET_ID && supported_source? && - @data_source.supported? && !@data_source.entity.to_s.empty? && resolved_scope + widget_type&.fetch('WidgetId', nil) == WIDGET_ID && supported_source? && resolved_scope end def render # rubocop:disable Metrics/AbcSize, Metrics/MethodLength @@ -50,21 +49,38 @@ def render # rubocop:disable Metrics/AbcSize, Metrics/MethodLength def supported_source? return database_source_supported? if primitive('source') == 'database' + return enumeration_source_supported? if primitive('optionsSourceType') == 'enumeration' association_source_supported? end def database_source_supported? - target_attribute && database_caption_attribute && database_value_attribute + target_attribute && database_caption_attribute && database_value_attribute && list_source_supported? end def association_source_supported? primitive('source') == 'context' && primitive('optionsSourceType') == 'association' && - association_step && (association_caption_attribute || association_caption_expression) + association_step && (association_caption_attribute || association_caption_expression) && + list_source_supported? + end + + def enumeration_source_supported? + primitive('source') == 'context' && enumeration_attribute + end + + def list_source_supported? + @data_source.supported? && !@data_source.entity.to_s.empty? end def source_properties - primitive('source') == 'database' ? database_properties : association_properties + return database_properties if primitive('source') == 'database' + return enumeration_properties if primitive('optionsSourceType') == 'enumeration' + + association_properties + end + + def enumeration_properties + { attributeEnumeration: raw(attribute_property(enumeration_attribute)) } end def database_properties @@ -192,6 +208,7 @@ def page_parameter_entity(name) def target_value = value('databaseAttributeString') def target_attribute = target_value&.fetch('AttributeRef', nil) + def enumeration_attribute = value('attributeEnumeration')&.fetch('AttributeRef', nil) def database_caption_attribute = value('optionsSourceDatabaseCaptionAttribute')&.fetch('AttributeRef', nil) def database_value_attribute = value('optionsSourceDatabaseValueAttribute')&.fetch('AttributeRef', nil) def association_caption_attribute = value('optionsSourceAssociationCaptionAttribute')&.fetch('AttributeRef', nil) diff --git a/lib/mxrb/compiler/compatibility_analyzer.rb b/lib/mxrb/compiler/compatibility_analyzer.rb index 608e096..d55cc46 100644 --- a/lib/mxrb/compiler/compatibility_analyzer.rb +++ b/lib/mxrb/compiler/compatibility_analyzer.rb @@ -52,7 +52,7 @@ def analyze_features end def analyze_version - @adapter = Adapter.for(@source.version) + @adapter = Adapter.for(@source.version, source: @source) rescue UnsupportedVersion => e add(:error, :version, "Mendix #{@source.version}", 'project', e.message) end diff --git a/lib/mxrb/compiler/deployment_bootstrapper.rb b/lib/mxrb/compiler/deployment_bootstrapper.rb index b1f0ad7..49ed3fc 100644 --- a/lib/mxrb/compiler/deployment_bootstrapper.rb +++ b/lib/mxrb/compiler/deployment_bootstrapper.rb @@ -77,12 +77,16 @@ def copy_version_templates def web_template_roots return { 'react-web' => 'web' } if modern_react_web? + return { 'dojo-web' => 'web' } if @source.version.to_i == 10 return { 'web' => 'web', 'modern-web' => 'modern-web' } if @source.version.to_i == 9 { 'web' => 'web' } end - def modern_react_web? = @source.version.to_i >= 11 + def modern_react_web? + @source.version.to_i >= 11 || (@source.version.to_i == 10 && @source.optimized_web_client?) + end + def legacy_archive? = @source.version.to_i <= 7 && File.file?(deployment_archive) def deployment_archive = File.join(@version_root, 'modeler', 'deployment.mxz') @@ -152,6 +156,7 @@ def replacements '{InstallDir:JsStringEncode}' => @version_root, '{InstallDir:HtmlAttributeEncode}' => @version_root, '{DeploymentDir:JsStringEncode}' => @deployment, + '{DeploymentDir:HtmlAttributeEncode}' => @deployment, '{ProjectDir:HtmlAttributeEncode}' => @project_root, '{ProjectName:HtmlAttributeEncode}' => project_name, '{EnableWatchman:true|false}' => 'false', '{MxBuildNumber}' => "mxrb-#{@source.version}" diff --git a/lib/mxrb/compiler/legacy_page_builder.rb b/lib/mxrb/compiler/legacy_page_builder.rb index 96ab546..b1b9f70 100644 --- a/lib/mxrb/compiler/legacy_page_builder.rb +++ b/lib/mxrb/compiler/legacy_page_builder.rb @@ -7,7 +7,8 @@ module Mxrb module Compiler LegacyPageBuild = Data.define(:directory, :files, :bytes, :unsupported_widgets) - # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/MethodLength + # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity + # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity # Writes deterministic Dojo page XML for the built-in Data Grid 1 contract. class LegacyPageBuilder include ModelValues @@ -31,9 +32,13 @@ def initialize(source, web_root, profiles: [:dojo]) def build pages = @source.units_of('Forms$Page') - languages.each { |language| pages.each { write_page(_1, language) } } + layouts = @source.units_of('Forms$Layout') + languages.each do |language| + layouts.each { write_layout(_1, language) } + pages.each { write_page(_1, language) } + end write_manifest - files = Dir.glob(File.join(@web_root, 'pages', '**', '*.page.xml')).select { File.file?(_1) } + files = Dir.glob(File.join(@web_root, 'pages', '**', '*.xml')).select { File.file?(_1) } LegacyPageBuild.new( directory: File.join(@web_root, 'pages'), files: files.length, bytes: files.sum { File.size(_1) }, unsupported_widgets: frozen_unsupported @@ -43,7 +48,7 @@ def build # Performs the same widget audit as #build without writing deployment files. def audit @unsupported.clear - @source.units_of('Forms$Page').each do |unit| + (@source.units_of('Forms$Layout') + @source.units_of('Forms$Page')).each do |unit| name = "#{unit.module_name}.#{unit.document['Name']}" audit_unsupported_widgets(unit.document, name) descendants(unit.document).select { _1['$Type'] == 'Forms$DataGrid' }.each_with_index do |grid, index| @@ -60,16 +65,23 @@ def audit def write_page(unit, language) name = "#{unit.module_name}.#{unit.document['Name']}" audit_unsupported_widgets(unit.document, name) - grids = descendants(unit.document).select { _1['$Type'] == 'Forms$DataGrid' } - rendered = grids.each_with_index.map do |grid, index| - compiler = LegacyDataGridCompiler.new(@source, name, grid, language:, sequence: index + 1) - compiler.html.tap { @unsupported[name].concat(compiler.unsupported) } - end.join + @widget_sequence = 0 + rendered = page_widgets(unit.document).map { render_widget(_1, name, language) }.join path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.page.xml") FileUtils.mkdir_p(File.dirname(path)) File.binwrite(path, page_xml(unit, language, rendered)) end + def write_layout(unit, language) + name = "#{unit.module_name}.#{unit.document['Name']}" + audit_unsupported_widgets(unit.document, name) + @widget_sequence = 0 + rendered = layout_widgets(unit.document).map { render_widget(_1, name, language) }.join + path = File.join(@web_root, 'pages', language, unit.module_name, "#{unit.document['Name']}.layout.xml") + FileUtils.mkdir_p(File.dirname(path)) + File.binwrite(path, layout_xml(unit, rendered)) + end + def audit_unsupported_widgets(root, page_name) visit = lambda do |value| case value @@ -77,8 +89,14 @@ def audit_unsupported_widgets(root, page_name) type = value['$Type'].to_s if type == 'Forms$DataGrid' return - elsif VISUAL_WIDGET_TYPES.include?(type) + elsif VISUAL_WIDGET_TYPES.include?(type) && !supported_widget_type?(type) @unsupported[page_name] << type + elsif type == 'Forms$DynamicText' && !array(value.dig('Content', 'Parameters')).empty? + @unsupported[page_name] << 'Forms$DynamicText(parameters)' + elsif type == 'Forms$DivContainer' + action_type = value.dig('OnClickAction', '$Type').to_s + @unsupported[page_name] << 'Forms$DivContainer(onClick)' \ + unless action_type.empty? || action_type == 'Forms$NoAction' end value.each_value { visit.call(_1) } @@ -93,15 +111,134 @@ def page_xml(unit, language, content) title = translated(page['Title'], language) layout = page.dig('FormCall', 'Form').to_s.tr('.', '/') argument = array(page.dig('FormCall', 'Arguments')).first - parameter = argument&.fetch('Parameter', '').to_s + parameter = layout_argument_id(argument&.fetch('Parameter', '').to_s) + css = page_css_classes(page) + class_attribute = css.empty? ? '' : " class='#{escape(css)}'" "\uFEFF" \ "" \ + "title='#{escape(title)}'#{class_attribute} xmlns:m='http://schemas.mendix.com/forms/1.0'>" \ "#{layout.empty? ? '' : ""}" \ "#{content}" \ '' end + def layout_xml(unit, content) + "\uFEFF" \ + "" \ + "#{content}" + end + + def render_widget(widget, page_name, language) + return '' unless widget.is_a?(Hash) + + case widget['$Type'] + when 'Forms$VerticalFlow' + widget_children(widget).map { render_widget(_1, page_name, language) }.join + when 'Forms$Placeholder' + identifier = IO::BsonCodec.extract_id(widget['$ID']) || widget['Name'].to_s + "
" + when 'Forms$DivContainer' + render_container(widget, page_name, language) + when 'Forms$DynamicText' + render_dynamic_text(widget, language) + when 'Forms$DataGrid' + render_data_grid(widget, page_name, language) + else + '' + end + end + + def render_container(widget, page_name, language) + children = widget_children(widget).map { render_widget(_1, page_name, language) }.join + tag = widget['RenderMode'].to_s == 'Span' ? 'span' : 'div' + "<#{tag}#{html_attributes(widget, base: nil)}>#{children}" + end + + def render_dynamic_text(widget, language) + content = widget['Content'] || {} + text = translated(content['Template'], language) + text = translated(content['Fallback'], language) if text.empty? + tag = widget['RenderMode'].to_s == 'Paragraph' ? 'p' : 'span' + attributes = html_attributes(widget, base: 'mx-text', mendix_id: next_widget_id) + "<#{tag}#{attributes}>#{escape(text)}" + end + + def render_data_grid(widget, page_name, language) + compiler = LegacyDataGridCompiler.new( + @source, page_name, widget, language:, sequence: next_widget_sequence + ) + compiler.html.tap { @unsupported[page_name].concat(compiler.unsupported) } + end + + def html_attributes(widget, base:, mendix_id: nil) + values = {} + css = css_classes(widget, base:) + values['class'] = css unless css.empty? + style = widget['Style'].to_s + style = widget.dig('Appearance', 'Style').to_s if style.empty? + values['style'] = style unless style.empty? + values['data-mendix-id'] = mendix_id if mendix_id + values.map { |key, value| " #{key}='#{escape(value)}'" }.join + end + + def css_classes(widget, base:) + name = widget['Name'].to_s + classes = [base, ("mx-name-#{name}" unless name.empty?), widget['Class'], + widget.dig('Appearance', 'Class')] + classes.compact.flat_map { _1.to_s.split }.reject(&:empty?).uniq.join(' ') + end + + def page_css_classes(page) + layout = layout_unit(page.dig('FormCall', 'Form').to_s)&.document || {} + [layout['Class'], layout.dig('Appearance', 'Class'), page['Class'], + page.dig('Appearance', 'Class')].compact.flat_map { _1.to_s.split } + .reject(&:empty?).uniq.join(' ') + end + + def page_widgets(document) + argument = array(document.dig('FormCall', 'Arguments')).first + widget_children(argument || document) + end + + def layout_widgets(document) = widget_children(document) + + def widget_children(value) + return [] unless value.is_a?(Hash) + + plural = array(value['Widgets']) + singular = value['Widget'] + plural.empty? && singular.is_a?(Hash) ? [singular] : plural + end + + def supported_widget_type?(type) + %w[Forms$DataGrid Forms$DivContainer Forms$DynamicText].include?(type) + end + + def layout_argument_id(parameter) + module_name, layout_name, placeholder_name = parameter.split('.', 3) + return parameter unless placeholder_name + + layout = layout_unit("#{module_name}.#{layout_name}") + placeholder = descendants(layout&.document || {}).find do |value| + value['$Type'] == 'Forms$Placeholder' && value['Name'] == placeholder_name + end + IO::BsonCodec.extract_id(placeholder&.fetch('$ID', nil)) || parameter + end + + def layout_unit(qualified_name) + module_name, layout_name = qualified_name.split('.', 2) + @source.units_of('Forms$Layout').find do |unit| + unit.module_name == module_name && unit.document['Name'] == layout_name + end + end + + def next_widget_sequence + @widget_sequence += 1 + end + + def next_widget_id = "1_#{next_widget_sequence - 1}" + def descendants(root) result = [] visit = lambda do |value| @@ -144,6 +281,7 @@ def escape(value) value.to_s.gsub('&', '&').gsub("'", ''').gsub('<', '<').gsub('>', '>') end end - # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/MethodLength + # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity + # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity end end diff --git a/lib/mxrb/compiler/nanoflow_program_compiler.rb b/lib/mxrb/compiler/nanoflow_program_compiler.rb index a253b0a..a03ec7d 100644 --- a/lib/mxrb/compiler/nanoflow_program_compiler.rb +++ b/lib/mxrb/compiler/nanoflow_program_compiler.rb @@ -52,8 +52,8 @@ def compile(unit, qualified_name) @programs[qualified_name][:declaration] = "const #{reference} = #{js_literal(program)};" "() => #{reference}" ensure - @flow_stack&.pop - @variable_kind_stack&.pop + @flow_stack.pop + @variable_kind_stack.pop end def variable_kinds(document) @@ -201,7 +201,7 @@ def compile_change_object(action, node) def change_instructions(action, variable, label) array(action['Items']).map.with_index do |item, index| - member = item['Attribute'].to_s.split('.').last + member = item['Attribute'].to_s.split('.').last.to_s next unsupported!(item, item['$Type']) if member.empty? || item['Type'] != 'Set' { type: 'changeObject', label: index.zero? ? label : "#{label}$#{index}", @@ -333,7 +333,7 @@ def compile_show_message(action, node) end def compile_validation(action, node) - member = action['Attribute'].to_s.split('.').last + member = action['Attribute'].to_s.split('.').last.to_s return unsupported!(action, action['$Type']) if member.empty? { type: 'showValidation', label: model_id(node), inputVar: action['ValidationVariableName'], diff --git a/lib/mxrb/compiler/page_bundle_builder.rb b/lib/mxrb/compiler/page_bundle_builder.rb index ee56c37..087bd5d 100644 --- a/lib/mxrb/compiler/page_bundle_builder.rb +++ b/lib/mxrb/compiler/page_bundle_builder.rb @@ -18,7 +18,7 @@ def build FileUtils.mkdir_p(destination) Dir.glob(File.join(destination, '**', '*.js')).each { FileUtils.rm_f(_1) } bundles = @source.units_of('Forms$Page').select { web_page?(_1) } - .map { PageBundleCompiler.new(@source).compile(_1) } + .map { PageBundleCompiler.new(@source).compile(_1) } bundles.each { write_bundle(destination, _1) } layout_bundles = build_layouts write_manifest(bundles) @@ -41,7 +41,7 @@ def build_layouts FileUtils.mkdir_p(destination) Dir.glob(File.join(destination, '**', '*.js')).each { FileUtils.rm_f(_1) } bundles = @source.units_of('Forms$Layout').select { web_layout?(_1) } - .map { PageBundleCompiler.new(@source).compile_layout(_1) } + .map { PageBundleCompiler.new(@source).compile_layout(_1) } bundles.each { write_bundle(destination, _1) } bundles end diff --git a/lib/mxrb/compiler/page_bundle_compiler.rb b/lib/mxrb/compiler/page_bundle_compiler.rb index f4ac102..34e1187 100644 --- a/lib/mxrb/compiler/page_bundle_compiler.rb +++ b/lib/mxrb/compiler/page_bundle_compiler.rb @@ -15,6 +15,11 @@ def initialize(source) @source = source @unsupported = [] @unsupported_custom = [] + @data_view_scopes = [] + @list_scopes = [] + @snippet_scopes = [] + @snippet_stack = [] + @snippet_documents = [] @uses_conditional = false @uses_dynamic_class = false end @@ -42,6 +47,7 @@ def prepare_compile(unit, prefix) @list_scopes = [] @snippet_scopes = [] @snippet_stack = [] + @snippet_documents = [] @generic_widgets = {} @nanoflow_programs = NanoflowProgramCompiler.new(@source) end @@ -86,6 +92,7 @@ def render_widget(widget) when 'Forms$LayoutGrid' then render_layout_grid(widget) when 'Forms$LayoutGridRow' then render_grid_row(widget) when 'Forms$LayoutGridColumn' then render_grid_column(widget) + when 'Forms$Table' then render_table(widget) when 'Forms$DynamicText' then render_text(widget) when 'Forms$Title' then render_title(widget) when 'Forms$ActionButton' then render_action_button(widget) @@ -131,6 +138,38 @@ def render_grid_column(widget) render_element('div', widget, array(widget['Widgets']), classes) end + def render_table(widget) + cells = array(widget['Cells']).group_by { integer_or(_1['TopRowIndex'], 0) } + rows = array(widget['Rows']).map.with_index do |row, index| + content = cells.fetch(index, []).sort_by { integer_or(_1['LeftColumnIndex'], 0) } + .map { render_table_cell(_1) } + props = common_props(row).merge(className: css_class(row)) + "React.createElement(\"tr\", #{js_props(props)}, [#{content.join(', ')}])" + end + props = common_props(widget).merge( + className: ['mx-table', css_class(widget)].reject(&:empty?).join(' ') + ) + body = "React.createElement(\"tbody\", null, [#{rows.join(', ')}])" + "React.createElement(\"table\", #{js_props(props)}, #{body})" + end + + def render_table_cell(cell) + tag = cell['IsHeader'] == true ? 'th' : 'td' + props = common_props(cell).merge( + className: css_class(cell), + colSpan: positive_integer(cell['Width'], 1), rowSpan: positive_integer(cell['Height'], 1), + 'data-column-index': integer_or(cell['LeftColumnIndex'], 0) + ) + content = children(array(cell['Widgets'])) + "React.createElement(#{JSON.generate(tag)}, #{js_props(props)}, #{content})" + end + + def integer_or(value, fallback) + Integer(value) + rescue ArgumentError, TypeError + fallback + end + def render_scroll_container(widget) @uses_layout_widgets = true props = common_props(widget).merge( @@ -206,12 +245,12 @@ def menu_items(source) unit = @source.units_of('Menus$MenuDocument').find do |candidate| "#{candidate.module_name}.#{candidate.document['Name']}" == qualified end - array(unit&.document&.dig('ItemCollection', 'Items')) if unit + array(unit.document.dig('ItemCollection', 'Items')) if unit when 'Forms$NavigationSource' profile = @source.units_of('Navigation$NavigationDocument').flat_map do |unit| array(unit.document['Profiles']) end.find { _1['Name'] == source['NavigationProfile'] } - array(profile&.dig('Menu', 'Items')) if profile + array(profile.dig('Menu', 'Items')) if profile end end @@ -317,7 +356,7 @@ def render_custom_widget(widget) combo = ComboBoxBundleCompiler.new( @source, @qualified_name, widget, - scope: scope_name(@data_view_scopes&.last), entity: @data_view_scopes&.last&.fetch(:entity, nil) + scope: scope_name(@data_view_scopes.last), entity: @data_view_scopes.last&.fetch(:entity, nil) ) if combo.supported? @uses_form_widgets = true @@ -422,14 +461,17 @@ def render_snippet_call(widget) return render_unsupported(widget) unless snippet && !@snippet_stack.include?(qualified) @snippet_stack << qualified + @snippet_documents << snippet.document @snippet_scopes << snippet_scope_map(snippet.document) rendered = children(array(snippet.document['Widgets'])) @snippet_scopes.pop + @snippet_documents.pop @snippet_stack.pop "React.createElement(React.Fragment, #{js_props(common_props(widget))}, #{rendered})" ensure - if @snippet_stack&.last == qualified + if @snippet_stack.last == qualified @snippet_scopes.pop + @snippet_documents.pop @snippet_stack.pop end end @@ -508,7 +550,7 @@ def qualified_attribute?(attribute) end def current_object_scope - scope = @list_scopes&.last || @data_view_scopes&.last + scope = @list_scopes.last || @data_view_scopes.last return scope if scope.is_a?(Hash) return unless scope @@ -858,12 +900,38 @@ def microflow_argument(mapping) name = mapping['Parameter'].to_s.split('.').last expression = mapping['Expression'].to_s current_scope = current_object_scope&.fetch(:scope) - scope = expression == '$currentObject' ? current_scope : expression + scope = if expression == '$currentObject' + current_scope + elsif expression.empty? + microflow_variable_scope(mapping['Variable']) + else + expression + end return unless present_identifier?(name) && scope.to_s.match?(/\A\$[A-Za-z_]\w*\z|\Ap\./) [name.to_sym, { widget: scope, source: 'object' }] end + def microflow_variable_scope(variable) + return unless variable + + snippet = variable['SnippetParameter'].to_s + unless snippet.empty? + @snippet_scopes.reverse_each do |mapping| + return mapping.fetch(snippet).fetch(:scope) if mapping.key?(snippet) + end + end + page = variable['PageParameter'].to_s + return "$#{page}" if present_identifier?(page) + + widget = variable['Widget'].to_s + target = page_widget(widget) + return widget_key(target) if present_identifier?(widget) && target + + local = variable['LocalVariable'].to_s + "$#{local}" if present_identifier?(local) + end + def nanoflow_action?(action) action['$Type'] == 'Forms$CallNanoflowClientAction' && present_identifier?(action['Nanoflow']) && nanoflow_argument_map(action) && @@ -957,6 +1025,8 @@ def render_data_view(widget) def data_view_object_property(widget, scope) source = widget['DataSource'] || {} + return listen_object_property(source, widget) if source['$Type'] == 'Forms$ListenTargetSource' + source_scope = data_source_scope(source) path = entity_ref_path(source['EntityRef']) if source_scope @@ -974,8 +1044,45 @@ def data_view_object_property(widget, scope) nil end + def listen_object_property(source, widget) + target_name = source['ListenTarget'].to_s + target = page_widget(target_name) + return unless present_identifier?(target_name) && target + + @uses_listen_object = true + config = { + listenTo: widget_key(target), editable: true, + operationId: WebOperationCompiler.operation_id(@qualified_name, widget['Name']) + } + "ListenObjectProperty(#{js_literal(config)})" + end + + def page_widget(name) + roots = [*@snippet_documents.reverse, @unit.document] + roots.each do |root| + widget = all_page_widgets(root).find { _1['Name'] == name } + return widget if widget + end + nil + end + + def all_page_widgets(value, result = []) + case value + when Hash + result << value if value['$Type'].to_s.start_with?('Forms$', 'CustomWidgets$') + value.each_value { all_page_widgets(_1, result) } + when Array then value.each { all_page_widgets(_1, result) } + end + result + end + def data_view_entity(widget) source = widget['DataSource'] || {} + if source['$Type'] == 'Forms$ListenTargetSource' + target = page_widget(source['ListenTarget'].to_s) + return target ? WebListDataSource.new(@source, target).entity : '' + end + entity = entity_ref_destination(source['EntityRef']) return entity unless entity.empty? @@ -1746,6 +1853,8 @@ def widget_imports end imports << 'import { MicroflowObjectProperty } from "mendix/MicroflowObjectProperty";' \ if @uses_microflow_object + imports << 'import { ListenObjectProperty } from "mendix/ListenObjectProperty";' \ + if @uses_listen_object widgets << 'Container' if @uses_container end if @uses_combo_box diff --git a/lib/mxrb/compiler/portable_packager.rb b/lib/mxrb/compiler/portable_packager.rb index c1124f0..2d133e9 100644 --- a/lib/mxrb/compiler/portable_packager.rb +++ b/lib/mxrb/compiler/portable_packager.rb @@ -55,8 +55,8 @@ def studio_document(events, execution) ClientCertificatePasswords = "" HashAlgorithm = "BCRYPT:12" } - adminUser.password = ${?RUNTIME_ADMINUSER_PASSWORD} - debugger.password = "" + adminUser.password = "" + #{debugger_configuration} } logging = [ { @@ -69,9 +69,17 @@ def studio_document(events, execution) CONF end + def debugger_configuration + @metadata['RuntimeVersion'].to_s.to_i >= 11 ? ' debugger.password = ""' : '' + end + def default_configuration <<~CONF - runtime.params { DatabaseType = HSQLDB, DatabaseName = default } + runtime.params { + DatabaseType = HSQLDB + DatabaseName = default + DatabaseJdbcUrl = "jdbc:hsqldb:file:app/data/database/default" + } admin { adminPassword = "", port = 8090, addresses = [ localhost ] } runtime { http { port = 8080, addresses = [ "*" ] } @@ -101,6 +109,72 @@ def hocon(value) end end + # Portable assets used by Runtime distributions that do not ship PAD. + module PortableFallbackAssets + START = <<~'SH' + #!/bin/sh + set -eu + + ROOT_PATH=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) + if [ -n "${JAVA_HOME:-}" ] && [ -x "$JAVA_HOME/bin/java" ]; then + JAVA="$JAVA_HOME/bin/java" + else + JAVA=$(command -v java || true) + fi + if [ -z "$JAVA" ] || [ ! -x "$JAVA" ]; then + echo "Cannot find java; set JAVA_HOME or add java to PATH." >&2 + exit 1 + fi + + if [ -z "${M2EE_ADMIN_PASS:-}" ]; then + M2EE_ADMIN_PASS=$(od -An -N16 -tx1 /dev/urandom | tr -d ' \n') + export M2EE_ADMIN_PASS + fi + RUNTIME_ADMINUSER_PASSWORD="${RUNTIME_ADMINUSER_PASSWORD:-$M2EE_ADMIN_PASS}" + export RUNTIME_ADMINUSER_PASSWORD + + export MX_INSTALL_PATH="$ROOT_PATH/lib" + CONFIG="${CONFIG:-$ROOT_PATH/etc/Default}" + if [ "$#" -eq 0 ]; then + set -- "$CONFIG" + fi + cd "$ROOT_PATH" + exec "$JAVA" ${JAVA_OPTS:-} \ + -Dfile.encoding=UTF-8 \ + -Djava.io.tmpdir="${TMPDIR:-/tmp}" \ + -Djava.library.path="$MX_INSTALL_PATH/runtime/lib/x64;$ROOT_PATH/app/model/lib/userlib" \ + -jar "$MX_INSTALL_PATH/runtime/launcher/runtimelauncher.jar" \ + "$ROOT_PATH/app/." "$@" + SH + VARIABLES = <<~CONF + admin { + port = ${?ADMIN_PORT} + addresses = ${?ADMIN_ADDRESSES} + adminPassword = ${?M2EE_ADMIN_PASS} + } + runtime.http { + port = ${?RUNTIME_HTTP_PORT} + addresses = ${?RUNTIME_HTTP_ADDRESSES} + } + runtime.params { + DatabaseHost = ${?RUNTIME_PARAMS_DATABASEHOST} + DatabaseJdbcUrl = ${?RUNTIME_PARAMS_DATABASEJDBCURL} + DatabaseName = ${?RUNTIME_PARAMS_DATABASENAME} + DatabaseUserName = ${?RUNTIME_PARAMS_DATABASEUSERNAME} + DatabasePassword = ${?RUNTIME_PARAMS_DATABASEPASSWORD} + DatabaseType = ${?RUNTIME_PARAMS_DATABASETYPE} + ApplicationRootUrl = ${?RUNTIME_PARAMS_APPLICATIONROOTURL} + } + runtime.adminUser.password = ${?RUNTIME_ADMINUSER_PASSWORD} + CONF + EXAMPLE = <<~CONF + # Copy this file and pass its path to bin/start to override etc/Default. + runtime.params { DatabaseType = HSQLDB, DatabaseName = default } + admin { port = 8090, addresses = [ localhost ] } + runtime.http { port = 8080, addresses = [ "*" ] } + CONF + end + # Deterministic ZIP writer shared by portable package orchestration. class PortableArchiveWriter FIXED_TIME = Packager::FIXED_TIME @@ -140,7 +214,7 @@ def add_application(archive) def add_generated_files(archive) add_start_scripts(archive) %w[example.conf variables.conf].each do |name| - add_source(archive, File.join(@runtime, 'pad', 'etc', name), "etc/#{name}") + add_runtime_configuration(archive, name) end PortableConfiguration.new(@metadata).files.each do |path, content| add_string(archive, path, content) @@ -148,13 +222,32 @@ def add_generated_files(archive) end def add_start_scripts(archive) - Dir.glob(File.join(@runtime, 'pad', 'bin', '*.hbs')).sort.each do |source| + templates = Dir.glob(File.join(@runtime, 'pad', 'bin', '*.hbs')).sort + return add_fallback_start(archive) if templates.empty? + + templates.each do |source| name = File.basename(source, '.hbs') content = render_template(File.binread(source)) add_string(archive, "bin/#{name}", content, mode: name == 'start' ? 0o755 : 0o644) end end + def add_fallback_start(archive) + add_string(archive, 'bin/start', PortableFallbackAssets::START, mode: 0o755) + end + + def add_runtime_configuration(archive, name) + source = File.join(@runtime, 'pad', 'etc', name) + return add_source(archive, source, "etc/#{name}") if File.file?(source) + + fallback = if name == 'variables.conf' + PortableFallbackAssets::VARIABLES + else + PortableFallbackAssets::EXAMPLE + end + add_string(archive, "etc/#{name}", fallback) + end + def render_template(content) rendered = content.start_with?("\xEF\xBB\xBF".b) ? content.byteslice(3..) : content rendered.force_encoding(Encoding::UTF_8) @@ -206,12 +299,7 @@ def zip_entry(destination, mode) # Creates an executable portable application from materialized app files # and an installed Mendix Runtime distribution. It never invokes mxbuild. class PortablePackager - REQUIRED_RUNTIME_FILES = %w[ - launcher/runtimelauncher.jar - pad/bin/start.hbs - pad/etc/example.conf - pad/etc/variables.conf - ].freeze + REQUIRED_RUNTIME_FILES = %w[launcher/runtimelauncher.jar].freeze def initialize(mpr_path, deployment: nil, mendix_home: nil) @mpr_path = File.expand_path(mpr_path) diff --git a/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json b/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json new file mode 100644 index 0000000..984bf9c --- /dev/null +++ b/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json @@ -0,0 +1,1052 @@ +{ + "Constants$Constant": [ + "$ID", + "$Type", + "Name", + "QualifiedName", + "DataType", + "ExposedToClient" + ], + "DocumentTemplates$ConditionSettings": [ + "$ID", + "$Type", + "NewConditions", + "Attribute" + ], + "DocumentTemplates$DataView": [ + "$ID", + "$Type", + "Contents", + "Name", + "EntityPath", + "Microflow" + ], + "DocumentTemplates$DataViewDropZone": [ + "$ID", + "$Type", + "Widget" + ], + "DocumentTemplates$DocumentTemplate": [ + "$ID", + "$Type", + "Toplevels", + "Style", + "Header", + "Footer", + "Name", + "QualifiedName", + "PageWidth", + "PageHeight", + "PPI", + "MarginLeftInInch", + "MarginRightInInch", + "MarginTopInInch", + "MarginBottomInInch", + "ShowHeaderFooterFirstPage" + ], + "DocumentTemplates$DynamicLabel": [ + "$ID", + "$Type", + "Style", + "FormattingInfo", + "Name", + "AttributePath", + "RenderHTML" + ], + "DocumentTemplates$StaticLabel": [ + "$ID", + "$Type", + "Caption", + "Style", + "Name" + ], + "DocumentTemplates$Style": [ + "$ID", + "$Type", + "StyleAsString" + ], + "DocumentTemplates$Table": [ + "$ID", + "$Type", + "Rows", + "Style", + "Name", + "ColumnWeights", + "CellSpacing", + "CellPadding" + ], + "DocumentTemplates$TableCell": [ + "$ID", + "$Type", + "Widget", + "Style", + "ColSpan", + "RowSpan", + "IsPartOfSpan" + ], + "DocumentTemplates$TableRow": [ + "$ID", + "$Type", + "Cells", + "ConditionSettings" + ], + "DomainModels$AccessRule": [ + "$ID", + "$Type", + "MemberAccesses", + "AllowedUserRoles", + "AllowCreate", + "AllowDelete", + "XPathConstraint" + ], + "DomainModels$Association": [ + "$ID", + "$Type", + "DeleteBehavior", + "Source", + "GUID", + "Type", + "Owner", + "StorageFormat", + "ParentPointer", + "QualifiedName", + "UnqualifiedName", + "Navigability", + "ChildPointer" + ], + "DomainModels$Attribute": [ + "$ID", + "$Type", + "Value", + "Type", + "Name", + "GUID" + ], + "DomainModels$AutoNumberAttributeType": [ + "$ID", + "$Type" + ], + "DomainModels$BinaryAttributeType": [ + "$ID", + "$Type" + ], + "DomainModels$BooleanAttributeType": [ + "$ID", + "$Type" + ], + "DomainModels$CalculatedValue": [ + "$ID", + "$Type", + "Microflow", + "PassEntity" + ], + "DomainModels$DateTimeAttributeType": [ + "$ID", + "$Type", + "LocalizeDate" + ], + "DomainModels$DecimalAttributeType": [ + "$ID", + "$Type" + ], + "DomainModels$DeleteBehavior": [ + "$ID", + "$Type", + "ParentErrorMessage", + "ChildErrorMessage", + "ParentDeleteBehavior", + "ChildDeleteBehavior" + ], + "DomainModels$DomainModel": [ + "$ID", + "$Type", + "Entities", + "Associations", + "CrossAssociations" + ], + "DomainModels$EntityImpl": [ + "$ID", + "$Type", + "MaybeGeneralization", + "Attributes", + "ValidationRules", + "Events", + "Indexes", + "AccessRules", + "Source", + "GUID", + "Image", + "QualifiedName", + "UnqualifiedName" + ], + "DomainModels$EntityIndex": [ + "$ID", + "$Type", + "Attributes", + "GUID", + "IncludeInOffline" + ], + "DomainModels$EnumerationAttributeType": [ + "$ID", + "$Type", + "Enumeration" + ], + "DomainModels$Generalization": [ + "$ID", + "$Type", + "Persistable", + "HasCreatedDateAttr", + "HasChangedDateAttr", + "HasOwnerAttr", + "HasChangedByAttr", + "Generalization" + ], + "DomainModels$HashedStringAttributeType": [ + "$ID", + "$Type" + ], + "DomainModels$IndexedAttribute": [ + "$ID", + "$Type", + "Type", + "AttributePointer", + "Ascending" + ], + "DomainModels$IntegerAttributeType": [ + "$ID", + "$Type" + ], + "DomainModels$LongAttributeType": [ + "$ID", + "$Type" + ], + "DomainModels$MemberAccess": [ + "$ID", + "$Type", + "Attribute", + "Association", + "AccessRights" + ], + "DomainModels$NoGeneralization": [ + "$ID", + "$Type", + "Key", + "Persistable", + "HasCreatedDateAttr", + "HasChangedDateAttr", + "HasOwnerAttr", + "HasChangedByAttr", + "Generalization" + ], + "DomainModels$RequiredRuleInfo": [ + "$ID", + "$Type" + ], + "DomainModels$StoredValue": [ + "$ID", + "$Type", + "DefaultValue" + ], + "DomainModels$StringAttributeType": [ + "$ID", + "$Type", + "Length" + ], + "DomainModels$UniqueRuleInfo": [ + "$ID", + "$Type" + ], + "DomainModels$ValidationRule": [ + "$ID", + "$Type", + "Message", + "RuleInfo", + "Attribute" + ], + "Enumerations$Enumeration": [ + "$ID", + "$Type", + "Values", + "Name", + "QualifiedName" + ], + "Enumerations$EnumerationValue": [ + "$ID", + "$Type", + "Caption", + "RemoteValue", + "Name", + "Image" + ], + "Forms$DataGrid": [ + "$ID", + "$Type", + "ControlBar", + "Columns", + "Name", + "TabIndex", + "Entity", + "IsControlBarVisible", + "SelectionMode", + "SelectFirst", + "DefaultButtonTrigger", + "RefreshTime", + "NumberOfRows", + "ShowEmptyRows", + "TooltipForm" + ], + "Forms$DataGridAddButton": [ + "$ID", + "$Type", + "FormSettings" + ], + "Forms$DataGridColumn": [ + "$ID", + "$Type", + "Caption", + "FormattingInfo", + "AggregateCaption", + "AttributePath", + "ColumnKindString", + "ShowTooltip", + "AggregateFunction", + "Editable" + ], + "Forms$DataGridRemoveButton": [ + "$ID", + "$Type" + ], + "Forms$DataGridSelectButton": [ + "$ID", + "$Type" + ], + "Forms$DeleteClientAction": [ + "$ID", + "$Type", + "ClosePage" + ], + "Forms$FormAction": [ + "$ID", + "$Type", + "FormSettings", + "PagesForSpecializations" + ], + "Forms$FormSettings": [ + "$ID", + "$Type", + "TitleOverride", + "ParameterMappings", + "Form", + "Location" + ], + "Forms$FormattingInfo": [ + "$ID", + "$Type", + "DecimalPrecision", + "GroupDigits", + "EnumFormat", + "DateFormat", + "CustomDateFormat" + ], + "Forms$GridActionButton": [ + "$ID", + "$Type", + "Action" + ], + "Forms$GridControlBar": [ + "$ID", + "$Type", + "NewButtons", + "DefaultButtonPointer" + ], + "Forms$GridNewButton": [ + "$ID", + "$Type", + "FormSettings", + "Entity", + "EditLocation" + ], + "Forms$GridSearchButton": [ + "$ID", + "$Type" + ], + "Forms$Layout": [ + "$ID", + "$Type", + "Name", + "QualifiedName" + ], + "Forms$MicroflowAction": [ + "$ID", + "$Type", + "MicroflowSettings" + ], + "Forms$MicroflowSettings": [ + "$ID", + "$Type", + "ProgressMessage", + "ConfirmationInfo", + "Microflow", + "ProgressBar", + "Asynchronous", + "FormValidations", + "ExpectListArgument" + ], + "Forms$Page": [ + "$ID", + "$Type", + "Parameters", + "Title", + "UrlSegments", + "Name", + "QualifiedName", + "ModelerAllowedUserRoles", + "PopupWidth", + "PopupHeight", + "PopupResizable", + "Url" + ], + "Forms$PageParameter": [ + "$ID", + "$Type", + "Name" + ], + "Forms$PageParameterMapping": [ + "$ID", + "$Type", + "Parameter", + "Argument" + ], + "Forms$ReferenceSetSelector": [ + "$ID", + "$Type", + "ControlBar", + "Columns", + "Name", + "TabIndex", + "Entity", + "IsControlBarVisible", + "SelectionMode", + "SelectFirst", + "DefaultButtonTrigger", + "RefreshTime", + "NumberOfRows", + "ShowEmptyRows", + "TooltipForm", + "SelectableXPathConstraint" + ], + "Forms$Snippet": [ + "$ID", + "$Type", + "Name", + "QualifiedName" + ], + "Forms$WebUIProjectSettingsPart": [ + "$ID", + "$Type", + "UrlPrefix" + ], + "Images$Image": [ + "$ID", + "$Type", + "Name", + "Image", + "Format", + "QualifiedName" + ], + "Images$ImageCollection": [ + "$ID", + "$Type", + "Images", + "Name", + "QualifiedName" + ], + "JavaActions$JavaAction": [ + "$ID", + "$Type", + "Parameters", + "Name", + "QualifiedName", + "ReturnType" + ], + "JavaActions$JavaActionParameter": [ + "$ID", + "$Type", + "Name", + "Type" + ], + "Menus$MenuDocument": [ + "$ID", + "$Type", + "Name", + "QualifiedName" + ], + "Microflows$ActionActivity": [ + "$ID", + "$Type", + "Action", + "Caption", + "Disabled" + ], + "Microflows$AggregateAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "AggregateVariableName", + "Attribute", + "Expression", + "UseExpression", + "AggregateFunction", + "VariableName", + "Type", + "ReduceInitialValueExpression" + ], + "Microflows$Annotation": [ + "$ID", + "$Type" + ], + "Microflows$AssociationRetrieveSource": [ + "$ID", + "$Type", + "Type", + "StartVariableName", + "AssociationId" + ], + "Microflows$BasicCodeActionParameterValue": [ + "$ID", + "$Type", + "ValueExpression" + ], + "Microflows$CastAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "VariableName" + ], + "Microflows$ChangeAction": [ + "$ID", + "$Type", + "Items", + "ErrorHandlingType", + "RefreshInClient", + "Commit", + "ChangeVariableName" + ], + "Microflows$ChangeActionItem": [ + "$ID", + "$Type", + "Attribute", + "Association", + "Type", + "Value" + ], + "Microflows$ChangeListAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "ChangeVariableName", + "Value", + "Type" + ], + "Microflows$CloseFormAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "NumberOfPagesToClose" + ], + "Microflows$CommitAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "WithEvents", + "CommitVariableName", + "RefreshInClient" + ], + "Microflows$ConstantRange": [ + "$ID", + "$Type", + "SingleObject" + ], + "Microflows$CreateChangeAction": [ + "$ID", + "$Type", + "Items", + "ErrorHandlingType", + "RefreshInClient", + "Commit", + "Entity", + "VariableName" + ], + "Microflows$CreateListAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "Entity", + "VariableName" + ], + "Microflows$CreateVariableAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "VariableName", + "Type", + "InitialValue" + ], + "Microflows$DatabaseRetrieveSource": [ + "$ID", + "$Type", + "Range", + "NewSortings", + "Type", + "Entity", + "XpathConstraint" + ], + "Microflows$DeleteAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "DeleteVariableName", + "RefreshInClient" + ], + "Microflows$DocumentTemplateParameterMapping": [ + "$ID", + "$Type", + "WidgetName", + "Argument" + ], + "Microflows$DownloadFileAction": [ + "$ID", + "$Type", + "ErrorHandlingType", + "FileDocumentVariableName", + "ShowFileInBrowser" + ], + "Microflows$EndEvent": [ + "$ID", + "$Type", + "ReturnValue" + ], + "Microflows$EnumerationCase": [ + "$ID", + "$Type", + "StringRepresentation", + "Value" + ], + "Microflows$ExclusiveMerge": [ + "$ID", + "$Type" + ], + "Microflows$ExclusiveSplit": [ + "$ID", + "$Type", + "SplitCondition", + "Caption", + "ErrorHandlingType" + ], + "Microflows$ExpressionSplitCondition": [ + "$ID", + "$Type", + "Expression" + ], + "Microflows$GenerateDocumentAction": [ + "$ID", + "$Type", + "ParameterMappings", + "ErrorHandlingType", + "FileVariableName", + "LanguageVariableName", + "DocumentType", + "LanguageSetting", + "DocumentTemplate", + "OverrideTopMargin", + "OverrideBottomMargin", + "OverrideLeftMargin", + "OverrideRightMargin", + "MarginLeftInInch", + "MarginRightInInch", + "MarginTopInInch", + "MarginBottomInInch" + ], + "Microflows$InheritanceCase": [ + "$ID", + "$Type", + "StringRepresentation", + "Value" + ], + "Microflows$InheritanceSplit": [ + "$ID", + "$Type", + "SplitVariableName", + "Caption" + ], + "Microflows$IterableList": [ + "$ID", + "$Type", + "ListVariableName", + "VariableName" + ], + "Microflows$JavaActionCallAction": [ + "$ID", + "$Type", + "QueueSettings", + "ParameterMappings", + "ErrorHandlingType", + "JavaAction", + "ResultVariableName" + ], + "Microflows$JavaActionParameterMapping": [ + "$ID", + "$Type", + "Value", + "Parameter" + ], + "Microflows$LogMessageAction": [ + "$ID", + "$Type", + "MessageTemplate", + "ErrorHandlingType", + "Level", + "Node", + "IncludeLatestStackTrace" + ], + "Microflows$LoopedActivity": [ + "$ID", + "$Type", + "ObjectCollection", + "LoopSource", + "ErrorHandlingType" + ], + "Microflows$Microflow": [ + "$ID", + "$Type", + "ObjectCollection", + "SequenceFlows", + "ConcurrenyErrorMessage", + "UrlSegments", + "Name", + "QualifiedName", + "ReturnType", + "ApplyEntityAccess", + "ModelerAllowedUserRoles", + "AllowConcurrentExecution", + "ConcurrencyErrorMicroflow", + "Url", + "UrlSearchParameters" + ], + "Microflows$MicroflowCall": [ + "$ID", + "$Type", + "QueueSettings", + "ParameterMappings", + "Microflow" + ], + "Microflows$MicroflowCallAction": [ + "$ID", + "$Type", + "MicroflowCall", + "ErrorHandlingType", + "ResultVariableName" + ], + "Microflows$MicroflowCallParameterMapping": [ + "$ID", + "$Type", + "Parameter", + "Argument" + ], + "Microflows$MicroflowObjectCollection": [ + "$ID", + "$Type", + "Objects" + ], + "Microflows$MicroflowParameter": [ + "$ID", + "$Type", + "Name", + "Type", + "DefaultValue" + ], + "Microflows$NoCase": [ + "$ID", + "$Type", + "StringRepresentation" + ], + "Microflows$RetrieveAction": [ + "$ID", + "$Type", + "RetrieveSource", + "ErrorHandlingType", + "ResultVariableName" + ], + "Microflows$RetrieveSorting": [ + "$ID", + "$Type", + "AttributePath", + "SortOrder" + ], + "Microflows$Rule": [ + "$ID", + "$Type", + "ObjectCollection", + "SequenceFlows", + "Name", + "QualifiedName", + "ReturnType", + "ApplyEntityAccess" + ], + "Microflows$RuleCall": [ + "$ID", + "$Type", + "ParameterMappings", + "Microflow" + ], + "Microflows$RuleCallParameterMapping": [ + "$ID", + "$Type", + "Parameter", + "Argument" + ], + "Microflows$RuleSplitCondition": [ + "$ID", + "$Type", + "RuleCall" + ], + "Microflows$SequenceFlow": [ + "$ID", + "$Type", + "CaseValues", + "OriginPointer", + "DestinationPointer", + "IsErrorHandler" + ], + "Microflows$ShowFormAction": [ + "$ID", + "$Type", + "FormSettings", + "ErrorHandlingType", + "NumberOfPagesToClose" + ], + "Microflows$ShowHomePageAction": [ + "$ID", + "$Type", + "ErrorHandlingType" + ], + "Microflows$ShowMessageAction": [ + "$ID", + "$Type", + "Template", + "ErrorHandlingType", + "Type", + "Blocking" + ], + "Microflows$SortingsList": [ + "$ID", + "$Type", + "Sortings" + ], + "Microflows$StartEvent": [ + "$ID", + "$Type" + ], + "Microflows$StringTemplate": [ + "$ID", + "$Type", + "Parameters", + "Text" + ], + "Microflows$TemplateParameter": [ + "$ID", + "$Type", + "Expression" + ], + "Microflows$TextTemplate": [ + "$ID", + "$Type", + "Parameters", + "Text" + ], + "Navigation$HomePage": [ + "$ID", + "$Type", + "Page", + "Microflow" + ], + "Navigation$NavigationDocument": [ + "$ID", + "$Type", + "Profiles", + "Grids", + "CustomWidgetModules", + "PluginWidgets" + ], + "Navigation$NavigationProfile": [ + "$ID", + "$Type", + "OfflineEntityConfigsRuntime", + "HomePage", + "HomeItems", + "AppTitle", + "LoginPageSettings", + "ProgressiveWebAppSettings", + "NotFoundHomepage", + "Name", + "IsOffline", + "Kind", + "AppIcon" + ], + "Projects$ModuleImpl": [ + "$ID", + "$Type", + "DomainModel", + "AllDocuments" + ], + "Projects$Project": [ + "$ID", + "$Type", + "DeploymentID", + "RuntimeVersion", + "ModelVersion", + "SprintrProjectName" + ], + "Queues$BasicQueueConfig": [ + "$ID", + "$Type", + "ParallelismExpression", + "ClusterWide" + ], + "Queues$Queue": [ + "$ID", + "$Type", + "Config", + "Name", + "QualifiedName" + ], + "RegularExpressions$RegularExpression": [ + "$ID", + "$Type", + "Name", + "QualifiedName", + "Expression" + ], + "Security$PasswordPolicySettings": [ + "$ID", + "$Type", + "MinimumLength", + "RequireMixedCase", + "RequireSymbol", + "RequireDigit" + ], + "Security$ProjectSecurity": [ + "$ID", + "$Type", + "UserRoles", + "DemoUsers", + "PasswordPolicySettings", + "SecurityLevel", + "AdminUserName", + "AdminUserRole", + "EnableDemoUsers", + "EnableGuestAccess", + "GuestUserRole", + "StrictMode" + ], + "Security$UserRole": [ + "$ID", + "$Type", + "GUID", + "Name", + "Description", + "IsSystemAdministrator", + "ManageableRoles", + "ManageUsersWithoutRoles", + "CheckSecurity" + ], + "Settings$CertificateSettings": [ + "$ID", + "$Type" + ], + "Settings$ConfigurationSettings": [ + "$ID", + "$Type" + ], + "Settings$ConventionSettings": [ + "$ID", + "$Type" + ], + "Settings$DistributionSettings": [ + "$ID", + "$Type" + ], + "Settings$IntegrationProjectSettingsPart": [ + "$ID", + "$Type", + "ObsoleteEnableUrlEncoding" + ], + "Settings$JarDeploymentSettings": [ + "$ID", + "$Type" + ], + "Settings$LanguageSettings": [ + "$ID", + "$Type", + "Languages", + "DefaultLanguageCode" + ], + "Settings$ModelSettings": [ + "$ID", + "$Type", + "AfterStartupMicroflow", + "BeforeShutdownMicroflow", + "HealthCheckMicroflow", + "FirstDayOfWeek", + "DefaultTimeZoneCode", + "ScheduledEventTimeZoneCode", + "HashAlgorithm", + "BcryptCost", + "RoundingMode", + "AllowUserMultipleSessions", + "EnableDataStorageOptimisticLocking", + "UseSystemContextForBackgroundTasks", + "UseDatabaseForeignKeyConstraints", + "UseOQLVersion2", + "SslCertificateAlgorithm" + ], + "Settings$ProjectSettings": [ + "$ID", + "$Type", + "Settings" + ], + "Settings$WorkflowsProjectSettingsPart": [ + "$ID", + "$Type", + "WorkflowOnStateChangeEvent", + "UsertaskOnStateChangeEvent", + "OnWorkflowEvent", + "UserEntity" + ], + "Texts$Language": [ + "$ID", + "$Type", + "Code", + "Description", + "RightToLeft", + "CheckCompleteness", + "CustomDateFormat", + "CustomTimeFormat", + "CustomDateTimeFormat" + ], + "Texts$SystemText": [ + "$ID", + "$Type", + "Text", + "InternalKey" + ], + "Texts$SystemTextCollection": [ + "$ID", + "$Type", + "SystemTexts" + ], + "Texts$Text": [ + "$ID", + "$Type" + ] +} diff --git a/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 b/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 new file mode 100644 index 0000000..a57217a --- /dev/null +++ b/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 @@ -0,0 +1,1135 @@ +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 +Dlloy7L+B1JQQ+fjlwMA diff --git a/lib/mxrb/compiler/security_materializer.rb b/lib/mxrb/compiler/security_materializer.rb index 32d7c97..e2f6923 100644 --- a/lib/mxrb/compiler/security_materializer.rb +++ b/lib/mxrb/compiler/security_materializer.rb @@ -28,7 +28,7 @@ def materialize def source_security mpr = IO::MprFile.open(@mpr_path, readonly: true) document = mpr.all_units.lazy.map { mpr.parse_contents(_1) } - .find { _1['$Type'] == 'Security$ProjectSecurity' } + .find { _1['$Type'] == 'Security$ProjectSecurity' } raise CompilationError, 'MPR has no project security document' unless document document diff --git a/lib/mxrb/compiler/source_model.rb b/lib/mxrb/compiler/source_model.rb index 8d6e709..1974c74 100644 --- a/lib/mxrb/compiler/source_model.rb +++ b/lib/mxrb/compiler/source_model.rb @@ -47,6 +47,12 @@ def web_layout?(unit) unit.document.dig('Content', '$Type') == 'Forms$WebLayoutContent' end + def optimized_web_client? + settings = documents('Settings$ProjectSettings').first + web = nested_values(settings).find { _1.is_a?(Hash) && _1['$Type'] == 'Forms$WebUIProjectSettingsPart' } + web&.fetch('UseOptimizedClient', nil).to_s == 'Yes' + end + def client_reference?(qualified_name) reference = /(?:\A|[^A-Za-z0-9_.])@#{Regexp.escape(qualified_name)}(?![A-Za-z0-9_.])/ units.any? do |unit| @@ -100,6 +106,14 @@ def deep_string_match?(value, pattern) end end + def nested_values(value) + case value + when Hash then [value, *value.values.flat_map { nested_values(_1) }] + when Array then value.flat_map { nested_values(_1) } + else [] + end + end + def index_document(value, index) case value when Hash diff --git a/lib/mxrb/compiler/system_model_seed.rb b/lib/mxrb/compiler/system_model_seed.rb index 9a37bb9..3212438 100644 --- a/lib/mxrb/compiler/system_model_seed.rb +++ b/lib/mxrb/compiler/system_model_seed.rb @@ -14,6 +14,7 @@ class SystemModelSeed ['7.0.0', '7.17.0', '7.5.0'], ['7.17.0', '8.0.0', '7.17.0'], ['9.0.0', '10.0.0', '9.6.1.29396'], + ['10.0.0', '11.0.0', '10.24.0.73019'], ['11.0.0', '12.0.0', '11.12.1'] ].map { |minimum, maximum, seed| [Gem::Version.new(minimum), Gem::Version.new(maximum), seed] } .freeze @@ -43,7 +44,7 @@ def self.seed_version_for(version) # rubocop:disable Metrics/AbcSize, Metrics/Me raise CompilationError, "no audited native System model seed for Mendix #{version}; " \ - 'supported families are 6.x, 7.x, 9.x, and 11.x' + 'supported families are 6.x, 7.x, 9.x, 10.x, and 11.x' rescue ArgumentError raise CompilationError, "invalid Mendix version #{version.inspect}" end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength diff --git a/lib/mxrb/compiler/system_queue_materializer.rb b/lib/mxrb/compiler/system_queue_materializer.rb index c9fed6f..162f356 100644 --- a/lib/mxrb/compiler/system_queue_materializer.rb +++ b/lib/mxrb/compiler/system_queue_materializer.rb @@ -45,7 +45,7 @@ def document(id, name, parallelism, config_id) def queue_config(id, parallelism) config = { '$ID' => id, '$Type' => 'Queues$BasicQueueConfig' } - return config.merge('Parallelism' => parallelism.to_i) if @version.to_i < 11 + return config.merge('Parallelism' => parallelism.to_i) if @version.to_i < 10 config.merge('ParallelismExpression' => parallelism, 'ClusterWide' => false) end diff --git a/lib/mxrb/compiler/web_bundle_builder.rb b/lib/mxrb/compiler/web_bundle_builder.rb index 553cc60..29dc946 100644 --- a/lib/mxrb/compiler/web_bundle_builder.rb +++ b/lib/mxrb/compiler/web_bundle_builder.rb @@ -21,20 +21,20 @@ def initialize(mpr_path, deployment:, mendix_home:) def build web = File.join(@deployment, 'web') - profiles = Adapter.for(@source.version).web_profiles + profiles = Adapter.for(@source.version, source: @source).web_profiles return LegacyPageBuilder.new(@source, web, profiles:).build unless profiles.include?(:react) prepare(web) - run_rspack(web) + run_bundler(web) WebShellMaterializer.new(web, version: @source.version).materialize result(web) end private - def run_rspack(web) - output, status = Open3.capture2e(environment(web), node, runner, chdir: web) - raise CompilationError, "Rspack web build failed:\n#{output}" unless status.success? + def run_bundler(web) + output, status = Open3.capture2e(environment(web), node, *bundler_arguments, chdir: web) + raise CompilationError, "#{bundler_name} web build failed:\n#{output}" unless status.success? end def prepare(web) @@ -94,7 +94,15 @@ def result(web) end def node = File.join(@version_root, 'modeler', 'tools', 'node', 'linux-x64', 'node') - def runner = File.join(@version_root, 'modeler', 'tools', 'node', 'rspack-runner.mjs') + def bundler_name = @source.version.to_i == 10 ? 'Rollup' : 'Rspack' + + def bundler_arguments + return [File.join(node_modules, 'rollup', 'dist', 'bin', 'rollup'), '--config'] if @source.version.to_i == 10 + + [File.join(@version_root, 'modeler', 'tools', 'node', 'rspack-runner.mjs')] + end + + def node_modules = File.join(@version_root, 'modeler', 'tools', 'node', 'node_modules') def environment(web) { @@ -102,7 +110,7 @@ def environment(web) 'SHOULD_GENERATE_EMBEDDED_INDEX' => 'false', 'MX_DEPLOYMENT_WEB_DIRECTORY' => web, 'MX_WEB_CLIENT_BUILD_LOG' => File.join(@deployment, 'log', 'mxrb-web-build.log'), - 'NODE_PATH' => File.join(@version_root, 'modeler', 'tools', 'node', 'node_modules') + 'NODE_PATH' => node_modules } end diff --git a/lib/mxrb/compiler/web_list_data_source.rb b/lib/mxrb/compiler/web_list_data_source.rb index a0399ec..e08dffa 100644 --- a/lib/mxrb/compiler/web_list_data_source.rb +++ b/lib/mxrb/compiler/web_list_data_source.rb @@ -64,7 +64,7 @@ def nanoflow? = !@nanoflow_source.nil? && !nanoflow_name.empty? && !@nanoflow.ni attr_reader :widget def database_source?(source) - %w[Forms$DatabaseSource Forms$NewListViewDatabaseSource] + %w[Forms$DatabaseSource Forms$NewListViewDatabaseSource Forms$ListViewXPathSource] .include?(source&.fetch('$Type', nil)) end diff --git a/lib/mxrb/compiler/web_operation_compiler.rb b/lib/mxrb/compiler/web_operation_compiler.rb index a46fcc3..1691a93 100644 --- a/lib/mxrb/compiler/web_operation_compiler.rb +++ b/lib/mxrb/compiler/web_operation_compiler.rb @@ -28,7 +28,7 @@ def initialize(source) def write(path) operations = @source.units_of('Forms$Page').select { web_page?(_1) } - .flat_map { page_operations(_1) } + .flat_map { page_operations(_1) } if @source.is_a?(SourceModel) operations.concat(@source.units_of('Forms$Layout').select { web_layout?(_1) } .flat_map { page_operations(_1) }) diff --git a/lib/mxrb/dsl/builder.rb b/lib/mxrb/dsl/builder.rb index 9a5ec83..a0963d1 100644 --- a/lib/mxrb/dsl/builder.rb +++ b/lib/mxrb/dsl/builder.rb @@ -244,7 +244,34 @@ def _widget_list = @children # Entry point for the project-definition DSL. # Used by Mxrb.define { ... } + module ConnectorDeclarations + def connector(protocol, version: nil, marketplace_id: nil) + connector_requests << Protocols.request(protocol, version:, marketplace_id:) + end + + def connector_plans(adapter: nil) + connector_requests.map do |request| + Protocols.plan( + request.protocol, mendix_version: @mendix_version, version: request.version, + marketplace_id: request.marketplace_id, adapter: + ) + end + end + + def validate_connector_requests! + return if connector_requests.empty? + + raise MarketplaceError, + 'connector declarations are preview-only; resolve and apply connector_plans ' \ + 'to an existing MPR with the official Marketplace adapter' + end + + def connector_requests = (@connector_requests ||= []) + end + class Builder + include ConnectorDeclarations + def initialize(path) @path = path @mendix_version = "10.18.0" @@ -323,6 +350,7 @@ def evaluate_dir(dir) end def build! + validate_connector_requests! validate! Writer.new(@path, definition).write! self @@ -343,6 +371,7 @@ def definition security: @security, navigation: @navigation, design_system: @design_system, + connectors: connector_requests.map(&:to_h), project_assets: @project_assets, native_units_path: @native_units_path, native_unit_overrides: @native_unit_overrides diff --git a/lib/mxrb/exporter.rb b/lib/mxrb/exporter.rb index 53400c0..76e855f 100644 --- a/lib/mxrb/exporter.rb +++ b/lib/mxrb/exporter.rb @@ -9,6 +9,11 @@ module Mxrb # Exports an MPR into an editable, layered Ruby source tree. class Exporter LAYERS = %w[domain application presentation infrastructure].freeze + MARKETPLACE_PROVENANCE = [ + ".mxrb/marketplace.lock.json", + ".mxrb/marketplace", + ".mxrb/marketplace-originals" + ].freeze EDITABLE_FLOW_OBJECT_TYPES = %w[ Microflows$StartEvent Microflows$EndEvent @@ -103,9 +108,10 @@ def export_app_structure def export_project_assets source_root = File.dirname(@mpr_path) - files = Model::DesignSystem::ASSET_DIRECTORIES.flat_map do |directory| + files = (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory| root = File.join(source_root, directory) - next [] unless File.directory?(root) + next [root] if File.file?(root) && !File.symlink?(root) + next [] unless File.directory?(root) && !File.symlink?(root) Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH) end.select { File.file?(_1) && !File.symlink?(_1) }.sort diff --git a/lib/mxrb/frontend/migrator.rb b/lib/mxrb/frontend/migrator.rb new file mode 100644 index 0000000..ce4b5aa --- /dev/null +++ b/lib/mxrb/frontend/migrator.rb @@ -0,0 +1,775 @@ +# frozen_string_literal: true + +require 'digest' +require 'json' + +module Mxrb + module Frontend + MigrationIssue = Data.define(:unit_id, :path, :kind, :message) + MigrationChange = Data.define( + :unit_id, :before_hash, :before, :after, :widgets, :layout_rows, :design_properties + ) + + # Immutable, fail-closed preview of frontend model migrations. + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + class MigrationPlan + attr_reader :path, :version, :changes, :issues + + def initialize(path:, version:, changes:, issues:) + @path = path + @version = version + @changes = changes.freeze + @issues = issues.freeze + @applied = false + end + + def safe? = issues.empty? + def applied? = @applied + def widgets = changes.sum(&:widgets) + def layout_rows = changes.sum(&:layout_rows) + def design_properties = changes.sum(&:design_properties) + + def apply! + raise SerializationError, blocked_message unless safe? + raise SerializationError, 'frontend migration plan was already applied' if applied? + + mpr = IO::MprFile.open(path) + mpr.transaction do + changes.each do |change| + current = mpr.unit(change.unit_id) + raise SerializationError, "frontend unit disappeared: #{change.unit_id}" unless current + raise SerializationError, "frontend unit changed after preview: #{change.unit_id}" unless + current['ContentsHash'] == change.before_hash + + mpr.update_unit(change.unit_id, change.after) + end + end + @applied = true + self + ensure + mpr&.close + end + + private + + def blocked_message + details = issues.map { "#{_1.kind} at #{_1.unit_id}#{_1.path}: #{_1.message}" } + "frontend migration is blocked: #{details.join('; ')}" + end + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + # Updates installed pluggable-widget schemas and legacy layout-grid weights + # directly in an MPR. Package XML is the source of truth; no Studio Pro or + # mx invocation is involved. + # rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength + # rubocop:disable Metrics/ParameterLists, Metrics/PerceivedComplexity + # rubocop:disable Lint/NonLocalExitFromIterator + class Migrator + SUPPORTED_MAJORS = [10, 11].freeze + STANDARD_TYPE_KEYS = %w[ + $ID $Type HelpUrl ObjectType OfflineCapable StudioCategory StudioProCategory + SupportedPlatform WidgetDescription WidgetId WidgetName WidgetNeedsEntityContext + WidgetPluginWidget + ].freeze + STANDARD_OBJECT_KEYS = %w[$ID $Type Properties TypePointer].freeze + # TabletWeight/PhoneWeight use -1 as a valid responsive inheritance + # sentinel. CE0535 concerns only the desktop Weight total. + WEIGHT_KEYS = %w[Weight].freeze + AUDITED_DATA_GRID_PACKAGE_SHA256 = + '8eabd99ef927b918dbb2bd5f05cf12dc14026b37e496bc1b0569c34369a0b713' + ACTIVE_VALUE_FIELDS = { + 'Action' => %w[Action Microflow Nanoflow], + 'Association' => %w[EntityRef SourceVariable], + 'Attribute' => %w[AttributeRef SourceVariable], + 'DataSource' => %w[DataSource XPathConstraint], + 'Expression' => %w[Expression SourceVariable], 'Icon' => %w[Icon], + 'Image' => %w[Image], 'Object' => %w[Objects], 'Selection' => %w[Selection], + 'TextTemplate' => %w[TextTemplate TranslatableValue], 'Widgets' => %w[Widgets] + }.freeze + + def self.plan(path) = new(path).plan + + def initialize(path) + @path = File.expand_path(path) + @root = File.dirname(@path) + @definitions = {} + end + + def plan + mpr = IO::MprFile.open(@path, readonly: true) + version = mpr.mendix_version.to_s + issues = [] + unless SUPPORTED_MAJORS.include?(version.to_i) + issues << MigrationIssue.new('', '', :unsupported_version, + "Mendix #{version.inspect}; supported majors are 10 and 11") + return MigrationPlan.new(path: @path, version:, changes: [], issues:) + end + validate_widget_packages!(issues) + + changes = mpr.all_units.filter_map do |unit| + before = mpr.parse_contents(unit) + after = deep_copy(before) + counts = { widgets: 0, layout_rows: 0, design_properties: 0 } + migrate_value!(after, unit.fetch('UnitID'), '$', issues, counts) + next if after == before + + MigrationChange.new( + unit.fetch('UnitID'), unit.fetch('ContentsHash'), before, after, + counts.fetch(:widgets), counts.fetch(:layout_rows), counts.fetch(:design_properties) + ) + end + MigrationPlan.new(path: @path, version:, changes:, issues:) + ensure + mpr&.close + end + + private + + def migrate_value!(value, unit_id, path, issues, counts) + case value + when Hash + migrate_design_properties!(value, unit_id, path, issues, counts) if value.key?('DesignProperties') + migrate_layout_row!(value, unit_id, path, issues, counts) if layout_row?(value) + migrate_widget!(value, unit_id, path, issues, counts) if custom_widget?(value) + value.each { |key, child| migrate_value!(child, unit_id, "#{path}.#{key}", issues, counts) } + when Array + value.each_with_index do |child, index| + migrate_value!(child, unit_id, "#{path}[#{index}]", issues, counts) + end + end + end + + def migrate_widget!(widget, unit_id, path, issues, counts) + widget_id = widget.dig('Type', 'WidgetId').to_s + return if widget_id.empty? + + definition = definition(widget_id) + unless definition + kind, message = definition_failure(widget_id) + return issue(issues, unit_id, path, kind, message) + end + + new_type, new_object = WidgetPackage.template(definition) + return unless safe_envelope?(widget, new_type, new_object, unit_id, path, issues) + + migrated = rebind_object( + widget['Object'], widget.dig('Type', 'ObjectType'), + new_object, new_type['ObjectType'], unit_id, path, issues + ) + return unless migrated + + apply_audited_package_migration!(migrated, new_type, package_digest(widget_id)) + schema_changed = canonical_schema(widget['Type']) != canonical_schema(new_type) + object_changed = canonical_schema(widget['Object']) != canonical_schema(migrated) + return unless schema_changed || object_changed + + widget['Type'] = new_type + widget['Object'] = migrated + counts[:widgets] += 1 + end + + def safe_envelope?(widget, new_type, new_object, unit_id, path, issues) + old_type = widget['Type'] + old_object = widget['Object'] + return issue(issues, unit_id, path, :malformed_widget, 'missing Type or Object') unless + old_type.is_a?(Hash) && old_object.is_a?(Hash) + + extra_type = old_type.keys - STANDARD_TYPE_KEYS - new_type.keys + extra_object = old_object.keys - STANDARD_OBJECT_KEYS - new_object.keys + if extra_type.any? + return issue(issues, unit_id, path, :unknown_widget_schema, + "unrecognized Type fields: #{extra_type.sort.join(', ')}") + end + if extra_object.any? + return issue(issues, unit_id, path, :unknown_widget_object, + "unrecognized Object fields: #{extra_object.sort.join(', ')}") + end + + true + end + + def rebind_object(old_object, old_type, new_object, new_type, unit_id, path, issues) + old_types = property_types(old_type) + new_types = property_types(new_type) + old_by_pointer = old_types.to_h { [id(_1['$ID']), _1] } + new_by_key = new_types.to_h { [_1['PropertyKey'].to_s, _1] } + old_values = property_values(old_object) + rebound = {} + + old_values.each do |property| + old_property_type = old_by_pointer[id(property['TypePointer'])] + unless old_property_type + return issue(issues, unit_id, path, :unknown_property_pointer, + "property pointer #{id(property['TypePointer']).inspect} is not in WidgetType") + end + + key = old_property_type['PropertyKey'].to_s + new_property_type = new_by_key[key] + old_value_type = old_property_type['ValueType'] || {} + unless new_property_type + next if default_widget_value?(property['Value'], old_value_type) + + return issue(issues, unit_id, path, :removed_configured_widget_property, + "installed schema removed configured property #{key.inspect}") + end + + new_value_type = new_property_type['ValueType'] || {} + value = if old_value_type['Type'] == new_value_type['Type'] + rebind_value( + property['Value'], old_value_type, new_value_type, + unit_id, "#{path}.#{key}", issues + ) + else + convert_value( + property['Value'], old_value_type, new_value_type, + unit_id, "#{path}.#{key}", issues + ) + end + return unless value + + rebound[key] = deep_copy(property).merge( + 'TypePointer' => new_property_type['$ID'], 'Value' => value + ) + end + + result = deep_copy(new_object) + result['$ID'] = old_object['$ID'] if old_object.key?('$ID') + defaults = property_values(new_object).to_h do |property| + type = new_types.find { id(_1['$ID']) == id(property['TypePointer']) } + property = neutral_new_property(property, type) + [type.fetch('PropertyKey').to_s, property] + end + clear_inactive_default_captions!(rebound, new_by_key) + added = new_types.filter_map do |type| + key = type['PropertyKey'].to_s + defaults[key] unless rebound.key?(key) + end + # Studio Pro preserves the previous object-property order and appends + # newly introduced properties, even when their schema declaration is + # located in the middle of the XML. CE0463 checks that storage order. + values = rebound.values + added + result['Properties'] = IO::BsonCodec.build_array(values, marker: array_marker(new_object['Properties'])) + result + end + + def neutral_new_property(property, _type) = deep_copy(property) + + def clear_inactive_default_captions!(properties, types) + properties.each do |key, property| + next unless key.end_with?('Caption') && property.dig('Value', 'TextTemplate') + + controller = properties[key.delete_suffix('Caption')] + next unless controller&.dig('Value', 'PrimitiveValue') == 'false' + + value_type = types.dig(key, 'ValueType') + next unless canonical_schema(property['Value']) == canonical_schema(default_value(value_type)) + + replacement = default_value(value_type) + replacement['$ID'] = property.dig('Value', '$ID') + replacement['TypePointer'] = value_type['$ID'] + replacement['TextTemplate'] = nil + property['Value'] = replacement + end + end + + def rebind_value(old_value, old_type, new_type, unit_id, path, issues) + return issue(issues, unit_id, path, :malformed_widget_value, 'property Value is not an object') unless + old_value.is_a?(Hash) + + result = normalized_value(old_value, new_type) + result['TypePointer'] = new_type['$ID'] + return result unless old_type['Type'] == 'Object' + + objects = array_items(old_value['Objects']) + if objects.empty? + result['Objects'] = IO::BsonCodec.build_array([], marker: array_marker(old_value['Objects'])) + return result + end + + old_object_type = old_type['ObjectType'] + new_object_type = new_type['ObjectType'] + unless old_object_type.is_a?(Hash) && new_object_type.is_a?(Hash) + return issue(issues, unit_id, path, :changed_widget_object, + 'nested Object schema is unavailable') + end + + objects = objects.map.with_index do |object, index| + rebound = rebind_object( + object, old_object_type, object_template(new_object_type), new_object_type, + unit_id, "#{path}[#{index}]", issues + ) + return unless rebound + + rebound + end + result['Objects'] = IO::BsonCodec.build_array(objects, marker: array_marker(old_value['Objects'])) + result + end + + def normalized_value(old_value, value_type) + result = default_value(value_type) + fields = ACTIVE_VALUE_FIELDS.fetch(value_type['Type'], %w[PrimitiveValue]) + fields.each do |field| + result[field] = normalize_source_variable(deep_copy(old_value[field])) if old_value.key?(field) + end + preserve_ids!(result, old_value) + result + end + + def convert_value(old_value, old_type, new_type, unit_id, path, issues) + if old_type['Type'] == 'Boolean' && new_type['Type'] == 'Expression' + primitive = old_value&.fetch('PrimitiveValue', nil) + if %w[true false].include?(primitive) && boolean_value_only?(old_value, old_type) + result = default_value(new_type) + result['Expression'] = primitive + result['TypePointer'] = new_type['$ID'] + preserve_ids!(result, old_value) + return result + end + end + + issue( + issues, unit_id, path, :changed_widget_property, + "property changed from #{old_type['Type']} to #{new_type['Type']} without a lossless conversion" + ) + end + + def boolean_value_only?(value, value_type) + expected = default_value(value_type) + actual = deep_copy(value) + actual['PrimitiveValue'] = expected['PrimitiveValue'] + canonical_schema(actual) == canonical_schema(expected) + end + + def object_template(object_type) + properties = property_types(object_type).reject { _1.dig('ValueType', 'Type') == 'System' }.map do |type| + value_type = type.fetch('ValueType') + property = { + type: value_type.fetch('Type'), default: value_type.fetch('DefaultValue', '').to_s, + translations: [], required: value_type.fetch('Required', false), + selection_types: array_items(value_type['SelectionTypes']) + } + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetProperty', + 'TypePointer' => type['$ID'], + 'Value' => WidgetPackage.allocate.send(:widget_value, value_type, property) + } + end + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetObject', + 'Properties' => IO::BsonCodec.build_array(properties, marker: 2), + 'TypePointer' => object_type['$ID'] + } + end + + def default_widget_value?(value, value_type) + canonical_schema(value) == canonical_schema(default_value(value_type)) + end + + def default_value(value_type) + translations = array_items(value_type['Translations']).map do |translation| + [translation['LanguageCode'].to_s, translation['Text'].to_s] + end + property = { + type: value_type.fetch('Type'), default: value_type.fetch('DefaultValue', '').to_s, + translations:, required: value_type.fetch('Required', false), + selection_types: array_items(value_type['SelectionTypes']) + } + WidgetPackage.allocate.send(:widget_value, value_type, property) + end + + def migrate_layout_row!(row, unit_id, path, issues, counts) + columns = array_items(row['Columns']) + return if columns.empty? + + replacements = {} + WEIGHT_KEYS.each do |key| + values = columns.map { _1[key] } + next if values.all? { _1.is_a?(Integer) && _1.positive? } && values.sum == 12 + + normalized = normalize_weights(values) + unless normalized + return issue(issues, unit_id, path, :unsafe_layout_weights, + "#{key} values cannot be normalized safely: #{values.inspect}") + end + + replacements[key] = normalized + end + return if replacements.empty? + + replacements.each do |key, values| + columns.each_with_index { |column, index| column[key] = values.fetch(index) } + end + counts[:layout_rows] += 1 + end + + def migrate_design_properties!(owner, unit_id, path, issues, counts) + raw = owner['DesignProperties'] + items = array_items(raw) + mapping = spacing_mappings + return if items.empty? + + changed = migrate_simple_design_aliases!(items) + retained = [] + compounds = items.select { compound_design_property?(_1) }.to_h { [_1['Key'], _1] } + items.each do |item| + old_name = design_property_name(item) + target = mapping[old_name] + unless target + retained << item + next + end + + compound = compounds[target.fetch(:key)] ||= compound_design_property(target.fetch(:key)) + properties = array_items(compound.dig('Value', 'Properties')) + existing = properties.find { _1['Key'] == target.fetch(:child) } + if existing && existing.dig('Value', 'Option') != target.fetch(:option) + issue(issues, unit_id, path, :conflicting_design_property, + "#{old_name.inspect} conflicts with #{target.fetch(:child).inspect}") + retained << item + next + end + unless existing + properties << option_design_property(target.fetch(:child), target.fetch(:option)) + compound['Value']['Properties'] = IO::BsonCodec.build_array(properties, marker: 2) + end + retained << compound unless retained.include?(compound) + changed += 1 + end + return if changed.zero? + + owner['DesignProperties'] = IO::BsonCodec.build_array(retained, marker: array_marker(raw)) + counts[:design_properties] += changed + end + + def spacing_mappings + return @spacing_mappings if defined?(@spacing_mappings) + + mappings = {} + Dir.glob(File.join(@root, 'themesource', '*', '{web,native}', 'design-properties.json')).sort.each do |path| + document = JSON.parse(File.read(path)) + document.each_value do |properties| + Array(properties).each { collect_spacing_mappings(_1, mappings) } + end + rescue JSON::ParserError + next + end + @spacing_mappings = mappings + end + + def migrate_simple_design_aliases!(items) + mappings = design_alias_mappings + items.count do |item| + option = item.dig('Value', 'Option') + target = mappings[[item['Key'], option]] + next false unless target + + item['Key'] = target.fetch(:key) + item['Value']['Option'] = target.fetch(:option) if target.key?(:option) + true + end + end + + def design_alias_mappings + return @design_alias_mappings if defined?(@design_alias_mappings) + + mappings = {} + design_property_documents.each do |document| + document.each_value do |properties| + Array(properties).each { collect_design_aliases(_1, mappings) } + end + end + @design_alias_mappings = mappings.reject { |_key, value| value == :conflict } + end + + def design_property_documents + paths = Dir.glob(File.join(@root, 'themesource', '*', '{web,native}', 'design-properties.json')).sort + paths.filter_map do |path| + JSON.parse(File.read(path)) + rescue JSON::ParserError + nil + end + end + + def collect_design_aliases(property, mappings) + return unless property.is_a?(Hash) && property['name'] && property['type'] != 'Spacing' + + new_key = property.fetch('name') + old_keys = Array(property['oldNames']) + options = Array(property['options']) + old_keys.each { add_design_alias(mappings, [_1, nil], key: new_key) } + options.each do |option| + new_option = option['name'] + old_options = Array(option['oldNames']) + old_options.each { add_design_alias(mappings, [new_key, _1], key: new_key, option: new_option) } + old_keys.each { add_design_alias(mappings, [_1, new_option], key: new_key, option: new_option) } + old_keys.product(old_options).each do |old_key, old_option| + add_design_alias(mappings, [old_key, old_option], key: new_key, option: new_option) + end + end + end + + def add_design_alias(mappings, source, target) + mappings[source] = :conflict if mappings.key?(source) && mappings[source] != target + mappings[source] ||= target.freeze + end + + def collect_spacing_mappings(property, mappings) + return unless property.is_a?(Hash) && property['type'] == 'Spacing' + + %w[margin padding].each do |mode| + Array(property[mode]).each do |size| + %w[top right bottom left].each do |direction| + Array(size.dig(direction, 'oldNames')).each do |old_name| + target = { + key: property.fetch('name'), child: "#{mode}-#{direction}", + option: size.fetch('name') + }.freeze + mappings[old_name] = target if !mappings.key?(old_name) || mappings[old_name] == target + end + end + end + end + end + + def design_property_name(item) + return unless item.is_a?(Hash) && item.dig('Value', '$Type') == 'Forms$OptionDesignPropertyValue' + + "#{item['Key']}::#{item.dig('Value', 'Option')}" + end + + def compound_design_property?(item) + item.is_a?(Hash) && item.dig('Value', '$Type') == 'Forms$CompoundDesignPropertyValue' + end + + def compound_design_property(key) + { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$DesignPropertyValue', 'Key' => key, + 'Value' => { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$CompoundDesignPropertyValue', + 'Properties' => IO::BsonCodec.build_array([], marker: 2) + } + } + end + + def option_design_property(key, option) + { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$DesignPropertyValue', 'Key' => key, + 'Value' => { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$OptionDesignPropertyValue', 'Option' => option + } + } + end + + def normalize_weights(values) + return unless values.all? { _1.is_a?(Integer) && !_1.zero? } + + positive_total = values.select(&:positive?).sum + return if positive_total >= 12 && values.any?(&:negative?) + return if positive_total > 12 + + negative = values.each_index.select { values[_1].negative? } + return if negative.empty? + + available = 12 - positive_total + weights = negative.map { values[_1].abs } + allocated = apportion(available, weights) + return if allocated.any?(&:zero?) + + values.map.with_index { |value, index| value.positive? ? value : allocated.fetch(negative.index(index)) } + end + + def apportion(total, weights) + denominator = weights.sum + exact = weights.map { Rational(total * _1, denominator) } + result = exact.map(&:floor) + remaining = total - result.sum + order = exact.each_index.sort_by { |index| [-(exact[index] - result[index]), index] } + order.first(remaining).each { result[_1] += 1 } + result + end + + def definition(widget_id) + return @definitions[widget_id] if @definitions.key?(widget_id) + + @package_digests ||= {} + matches = valid_widget_package_paths.filter_map do |package_path| + found = WidgetPackage.new(package_path).definition(widget_id) + [package_path, found] if found + rescue Zip::Error, REXML::ParseException => e + @invalid_widget_packages ||= {} + @invalid_widget_packages[package_path] = e + nil + end + if matches.one? + package_path, found = matches.first + @package_digests[widget_id] = Digest::SHA256.file(package_path).hexdigest + return @definitions[widget_id] = found + end + + @definition_failures ||= {} + @definition_failures[widget_id] = if matches.empty? + [:missing_widget_definition, + "no installed MPK defines #{widget_id.inspect}"] + else + paths = matches.map { relative_package_path(_1.first) } + [:ambiguous_widget_definition, + "multiple installed MPKs define #{widget_id.inspect}: " \ + "#{paths.join(', ')}"] + end + @definitions[widget_id] = nil + end + + def definition_failure(widget_id) + @definition_failures&.fetch( + widget_id, [:missing_widget_definition, "no installed MPK defines #{widget_id.inspect}"] + ) + end + + def validate_widget_packages!(issues) + @invalid_widget_packages = {} + widget_package_paths.each do |package_path| + WidgetPackage.new(package_path).definition('__mxrb_package_validation__') + rescue Zip::Error, REXML::ParseException => e + @invalid_widget_packages[package_path] = e + issue( + issues, '', ".widgets.#{File.basename(package_path)}", :invalid_widget_package, + "cannot read #{relative_package_path(package_path)}: #{e.class}: #{e.message}" + ) + end + end + + def widget_package_paths + @widget_package_paths ||= Dir.glob(File.join(@root, 'widgets', '*.mpk')).sort + end + + def valid_widget_package_paths + invalid = @invalid_widget_packages || {} + widget_package_paths.reject { invalid.key?(_1) } + end + + def relative_package_path(path) + Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s + end + + def package_digest(widget_id) + @package_digests&.fetch(widget_id, nil) + end + + def apply_audited_package_migration!(object, type, digest) + return unless digest == AUDITED_DATA_GRID_PACKAGE_SHA256 + + types = property_types(type['ObjectType']).to_h { [_1['PropertyKey'], _1] } + properties = property_values(object).each_with_object({}) do |property, result| + property_type = types.values.find { id(_1['$ID']) == id(property['TypePointer']) } + result[property_type['PropertyKey']] = property if property_type + end + pagination = properties.dig('pagination', 'Value', 'PrimitiveValue') + selection = properties.dig('itemSelection', 'Value', 'Selection') + clear_text_template!(properties['loadMoreButtonCaption']) unless pagination == 'loadMore' + clear_text_template!(properties['clearSelectionButtonLabel']) unless selection == 'Multi' + clear_text_template!(properties['singleSelectionColumnLabel']) unless selection == 'Single' + clear_audited_data_grid_column_texts!(properties['columns'], types['columns']) + end + + def clear_text_template!(property) + property['Value']['TextTemplate'] = nil if property + end + + def clear_audited_data_grid_column_texts!(columns_property, columns_type) + return unless columns_property && columns_type + + object_type = columns_type.dig('ValueType', 'ObjectType') + types = property_types(object_type).to_h { [id(_1['$ID']), _1] } + array_items(columns_property.dig('Value', 'Objects')).each do |column| + values = property_values(column).each_with_object({}) do |property, result| + type = types[id(property['TypePointer'])] + result[type['PropertyKey']] = property if type + end + content = values.dig('showContentAs', 'Value', 'PrimitiveValue') + clear_text_template!(values['dynamicText']) unless content == 'dynamicText' + clear_text_template!(values['exportValue']) unless content == 'customContent' + clear_text_template!(values['tooltip']) if content == 'customContent' + end + end + + def property_types(type) = array_items(type&.fetch('PropertyTypes', nil)) + def property_values(object) = array_items(object&.fetch('Properties', nil)) + def array_items(value) = IO::BsonCodec.parse_array(value)[:items] + def array_marker(value) = IO::BsonCodec.parse_array(value)[:marker] + def id(value) = IO::BsonCodec.extract_id(value) + def custom_widget?(value) = value['$Type'] == 'CustomWidgets$CustomWidget' + def layout_row?(value) = value['$Type'] == 'Forms$LayoutGridRow' + + def canonical_schema(value) + case value + when Hash + value.each_with_object({}) do |(key, child), result| + next if ['$ID', 'TypePointer'].include?(key) + + result[key] = canonical_schema(child) + end + when Array + value.map { canonical_schema(_1) } + else value + end + end + + def issue(issues, unit_id, path, kind, message) + issues << MigrationIssue.new(unit_id, path, kind, message) + false + end + + def deep_copy(value) + case value + when Hash then value.to_h { |key, child| [key, deep_copy(child)] } + when Array then value.map { deep_copy(_1) } + else value + end + end + + def preserve_ids!(target, source) + case target + when Hash + return target unless source.is_a?(Hash) + + target['$ID'] = source['$ID'] if target.key?('$ID') && source.key?('$ID') + target.each do |key, child| + preserve_ids!(child, source[key]) if source.key?(key) && key != 'TypePointer' + end + when Array + return target unless source.is_a?(Array) + + target.zip(source).each { |child, old_child| preserve_ids!(child, old_child) } + end + target + end + + def normalize_source_variable(value) + case value + when Hash + value.each_with_object({}) do |(key, child), result| + result[key] = normalize_source_variable(child) + if value['$Type'] == 'Forms$PageVariable' && !value.key?('SubKey') && key == 'SnippetParameter' + result['SubKey'] = '' + elsif value['$Type'] == 'Forms$CallNanoflowClientAction' && !value.key?('OutputMappings') && + key == 'Nanoflow' + result['OutputMappings'] = IO::BsonCodec.build_array([], marker: 3) + elsif value['$Type'] == 'Forms$MicroflowSettings' && !value.key?('OutputMappings') && + key == 'Microflow' + result['OutputMappings'] = IO::BsonCodec.build_array([], marker: 3) + end + end + when Array then value.map { normalize_source_variable(_1) } + else value + end + end + end + # rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength + # rubocop:enable Metrics/ParameterLists, Metrics/PerceivedComplexity + # rubocop:enable Lint/NonLocalExitFromIterator + end +end diff --git a/lib/mxrb/model/design_system.rb b/lib/mxrb/model/design_system.rb index 5e5f69d..bb211f3 100644 --- a/lib/mxrb/model/design_system.rb +++ b/lib/mxrb/model/design_system.rb @@ -41,7 +41,7 @@ def unresolved_references names = tokens.map(&:name).to_h { [_1, true] } tokens.flat_map do |token| token.value.scan(/var\((--[A-Za-z0-9_-]+)/).flatten.reject { names.key?(_1) } - .map do + .map do { token:, reference: _1 }.freeze diff --git a/lib/mxrb/model/entity.rb b/lib/mxrb/model/entity.rb index 3aded53..4ee59bb 100644 --- a/lib/mxrb/model/entity.rb +++ b/lib/mxrb/model/entity.rb @@ -137,7 +137,7 @@ def self.parse_access_rule(doc) association_ref = m["Association"].to_s attr_ref = association_ref.empty? ? m["Attribute"] : association_ref kind = association_ref.empty? ? :attribute : :association - { name: attr_ref.to_s.split(%r{[/.]}).last, + { name: attr_ref.to_s.split(%r{[/.]}).last, reference: attr_ref.to_s, rights: m["AccessRights"] || "None", kind: kind } end { diff --git a/lib/mxrb/official_marketplace.rb b/lib/mxrb/official_marketplace.rb index 1c3e528..a909d7c 100644 --- a/lib/mxrb/official_marketplace.rb +++ b/lib/mxrb/official_marketplace.rb @@ -9,6 +9,9 @@ require 'uri' require 'zip' require_relative 'official_marketplace/module_package_importer' +require_relative 'official_marketplace/widget_package_installer' +require_relative 'official_marketplace/lifecycle' +require_relative 'official_marketplace/dependency_resolver' module Mxrb # Installer for official/community Mendix packages, separate from Ruby modules. @@ -48,11 +51,32 @@ def self.lock(target) def self.verify(target) root = File.expand_path(target) lock(root).fetch('packages').to_h do |name, entry| - result = entry['kind'] == 'module' ? verify_module(root, name, entry) : verify_tree(root, entry) + result = case entry['kind'] + when 'module' then verify_module(root, name, entry) + when 'widget' then verify_widget(root, entry) + else verify_tree(root, entry) + end [name, result] end end + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def self.verify_widget(root, entry) + destination = File.join(root, entry.fetch('destination')) + archive = File.join(root, entry.fetch('archive')) + actual = File.file?(destination) ? Digest::SHA256.file(destination).hexdigest : nil + cached = File.file?(archive) ? Digest::SHA256.file(archive).hexdigest : nil + inventory = WidgetPackageInventory.read(destination) if actual == entry['sha256'] + identity = inventory&.name == entry['widget_name'] && inventory&.widget_ids == entry['widget_ids'] + { + valid: actual == entry['sha256'] && cached == entry['sha256'] && identity, + expected: entry['sha256'], actual:, cached:, identity: + } + rescue MarketplaceError + { valid: false, expected: entry['sha256'], actual:, cached:, identity: false } + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def self.verify_tree(root, entry) destination = File.join(root, entry.fetch('destination')) actual = File.directory?(destination) ? tree_digest(destination) : nil @@ -335,6 +359,8 @@ def repository_and_name(identifier) # Safely extracts GitHub or local MPK archives into the target project. class Installer # rubocop:disable Metrics/ClassLength ATLAS_VARIABLES_IMPORT = '@import "../../themesource/atlas_core/web/variables";' + IMPORTABLE_CONTENT_TYPES = %w[Module Service].freeze + DOWNLOADABLE_CONTENT_TYPES = (IMPORTABLE_CONTENT_TYPES + %w[Widget]).freeze def initialize(target:, client: HttpClient.new, mpr: nil) @target = File.expand_path(target) @@ -361,20 +387,58 @@ def pull_official(identifier, version: nil, mendix_version: nil, download_official(package, mpr, api) end + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + def update_official(identifier, version: nil, mendix_version: nil, + allow_vulnerable: false, apply: false, api: ContentApi.new) + lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: self) + _name, installed = lifecycle.installed(identifier) + content_id = installed['content_id'] || identifier + mpr = lifecycle.mpr_path(installed) + mendix_version ||= detect_mendix_version(mpr) + package = api.resolve(content_id, version:, mendix_version:, allow_vulnerable:) + validate_official_package!(package) + Dir.mktmpdir('mxrb-marketplace-update-') do |dir| + archive = api.download( + package.version_id, File.join(dir, 'package.mpk'), download_url: package.download_url + ) + plan = lifecycle.plan_update(identifier, archive, package) + plan.apply! if apply + plan + end + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + + def remove(identifier, apply: false) + plan = Lifecycle.new(target: @target, mpr: @mpr, installer: self).plan_remove(identifier) + plan.apply! if apply + plan + end + + def dependencies(identifier, apply: false, apply_resolved: false, api: ContentApi.new) + lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: self) + _name, entry = lifecycle.installed(identifier) + mpr = lifecycle.mpr_path(entry) + resolver = DependencyResolver.new( + target: @target, mpr:, installer: self, api:, + mendix_version: detect_mendix_version(mpr) + ) + resolver.resolve(identifier, apply:, apply_resolved:) + end + def download_official(package, mpr, api) Dir.mktmpdir('mxrb-marketplace-') do |dir| archive = api.download( package.version_id, File.join(dir, 'package.mpk'), download_url: package.download_url ) - import_module(archive, package, mpr) + install_official_archive(archive, package, mpr) end end def validate_official_package!(package) - return if package.content_type == 'Module' + return if DOWNLOADABLE_CONTENT_TYPES.include?(package.content_type) raise MarketplaceError, - "Marketplace content #{package.name.inspect} is #{package.content_type}, not a Module" + "Marketplace content #{package.name.inspect} is #{package.content_type}, not a Module, Service, or Widget" end def import(path, name: nil, mpr: @mpr) @@ -388,6 +452,21 @@ def import(path, name: nil, mpr: @mpr) private + def install_official_archive(archive, package, mpr) # rubocop:disable Metrics/MethodLength + case PackageEnvelope.kind(archive) + when :module + import_module(archive, package, mpr) + when :widget + unless package.content_type == 'Widget' + raise MarketplaceError, + "Marketplace #{package.content_type} archive declares a clientModule widget" + end + WidgetPackageInstaller.new(target: @target).install(archive, package) + else + raise MarketplaceError, 'unsupported official Marketplace package envelope' + end + end + def detect_mendix_version(mpr) file = IO::MprFile.open(mpr, readonly: true) file.mendix_version @@ -395,26 +474,44 @@ def detect_mendix_version(mpr) file&.close end - def import_module(archive, package, mpr) # rubocop:disable Metrics/MethodLength + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def import_module(archive, package, mpr) relative_target_path(mpr) - result = module_importer(archive, mpr, package).import! + inventory = ModulePackageInventory.read(archive) + originals, created_backups = preserve_asset_originals(inventory) + result = module_importer( + archive, mpr, package, protected_files: protected_package_assets(inventory.name) + ).import! ensure_atlas_theme_variables resolved = resolved_package(package, result) sha256 = Digest::SHA256.file(archive).hexdigest cached = cache_package(archive, resolved, sha256) - write_module_lock(resolved, mpr, result, cached, sha256) + write_module_lock(resolved, mpr, result, cached, sha256, originals) ModuleInstallation.new( resolved, File.expand_path(mpr), sha256, result.module_name, result.module_id, result.units, result.files, cached ) + rescue StandardError + Array(created_backups).each { FileUtils.rm_f(_1) } + raise end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength - def module_importer(archive, mpr, package) + def module_importer(archive, mpr, package, protected_files: []) ModulePackageImporter.new( - archive, mpr, target_root: @target, allow_model_upgrade: package.source == :mendix + archive, mpr, target_root: @target, allow_model_upgrade: package.source == :mendix, + protected_files: ) end + def protected_package_assets(package_name) + OfficialMarketplace.lock(@target).fetch('packages').flat_map do |name, entry| + next [] if name == package_name + + Array(entry['files']) + end.uniq + end + def ensure_atlas_theme_variables atlas = File.join(@target, 'themesource', 'atlas_core', 'web', '_variables.scss') return unless File.file?(atlas) @@ -450,7 +547,8 @@ def cache_package(archive, package, sha256) destination end - def write_module_lock(package, mpr, result, cached, digest) # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + def write_module_lock(package, mpr, result, cached, digest, originals) path = File.join(@target, '.mxrb', 'marketplace.lock.json') FileUtils.mkdir_p(File.dirname(path)) lock = OfficialMarketplace.lock(@target) @@ -458,10 +556,47 @@ def write_module_lock(package, mpr, result, cached, digest) # rubocop:disable Me 'kind' => 'module', 'version' => package.version, 'source' => package.source, 'repository' => package.repository, 'sha256' => digest, 'destination' => relative_target_path(mpr), 'archive' => relative_target_path(cached), - 'module_id' => result.module_id, 'units' => result.units, 'files' => result.files + 'module_id' => result.module_id, 'units' => result.units, 'files' => result.files, + 'asset_originals' => originals }.merge(official_lock_metadata(package)).compact write_lock_atomically(path, lock) end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def preserve_asset_originals(inventory) + existing = OfficialMarketplace.lock(@target).dig('packages', inventory.name) + legacy = Array(existing&.fetch('files', [])).to_h { [_1, nil] } + retained = existing&.fetch('asset_originals', legacy) || {} + originals = inventory.files.keys.to_h do |relative| + [relative, retained.key?(relative) ? retained[relative] : preserve_asset(relative, inventory.name)] + end + created = originals.values.compact.filter_map do |relative| + path = safe_target_path(relative) + path unless retained.value?(relative) + end + [originals, created] + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + def preserve_asset(relative, module_name) + source = safe_target_path(relative) + raise MarketplaceError, "package asset destination is a symbolic link: #{relative}" if File.symlink?(source) + return unless File.file?(source) + + backup = safe_target_path(File.join('.mxrb', 'marketplace-originals', valid_name(module_name), relative)) + FileUtils.mkdir_p(File.dirname(backup)) + FileUtils.cp(source, backup) + relative_target_path(backup) + end + + def safe_target_path(relative) + path = File.expand_path(File.join(@target, relative.to_s)) + prefix = "#{@target}#{File::SEPARATOR}" + raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix) + + path + end def official_lock_metadata(package) return {} unless package.respond_to?(:content_id) diff --git a/lib/mxrb/official_marketplace/content_api.rb b/lib/mxrb/official_marketplace/content_api.rb index 961e2a9..ace1e09 100644 --- a/lib/mxrb/official_marketplace/content_api.rb +++ b/lib/mxrb/official_marketplace/content_api.rb @@ -9,6 +9,7 @@ class ContentApi # rubocop:disable Metrics/ClassLength CONTENT_LIMIT = 100 VERSION_LIMIT = 20 MENDIX_VERSION = /\A(?:\d{1,3}|1000)(?:\.(?:\d{1,3}|1000)){0,2}\z/ + MENDIX_BUILD_VERSION = /\A(?:\d{1,3}|1000)(?:\.(?:\d{1,3}|1000)){2}\.\d{1,10}\z/ UUID = /\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i def initialize(pat: nil, credentials: Credentials.new, client: HttpClient.new) @@ -205,9 +206,11 @@ def uuid_filter(value) def mendix_version_filter(value) return if value.nil? - raise MarketplaceError, "invalid Mendix version #{value.inspect}" unless value.to_s.match?(MENDIX_VERSION) - value.to_s + valid = value.to_s.match?(MENDIX_VERSION) || value.to_s.match?(MENDIX_BUILD_VERSION) + raise MarketplaceError, "invalid Mendix version #{value.inspect}" unless valid + + value.to_s.split('.').first(3).join('.') end def bounded_integer(value, range) diff --git a/lib/mxrb/official_marketplace/dependency_resolver.rb b/lib/mxrb/official_marketplace/dependency_resolver.rb new file mode 100644 index 0000000..7247f8c --- /dev/null +++ b/lib/mxrb/official_marketplace/dependency_resolver.rb @@ -0,0 +1,329 @@ +# frozen_string_literal: true + +module Mxrb + module OfficialMarketplace + Dependency = Data.define(:module_name, :package, :archive, :dependencies) + WidgetDependency = Data.define(:package, :archive, :widget_ids, :required_ids) + + # Preview for a recursively verified dependency installation. + class DependencyPlan + attr_reader :root, :dependencies, :widget_dependencies, :blockers + + def initialize(root:, dependencies:, blockers:, widget_dependencies: [], &operation) + @root = root + @dependencies = dependencies.freeze + @widget_dependencies = widget_dependencies.freeze + @blockers = blockers.freeze + @operation = operation + @applied = false + end + + def safe? = blockers.empty? + def applied? = @applied + + def changes + dependencies.map { "install #{_1.module_name} #{_1.package.version}" } + + widget_dependencies.map { "install widget bundle #{_1.package.name} #{_1.package.version}" } + end + + def apply! + raise MarketplaceError, 'Marketplace dependency plan was already applied' if applied? + raise MarketplaceError, "Marketplace dependency plan is blocked: #{blockers.join('; ')}" unless safe? + raise MarketplaceError, 'Marketplace dependency preview expired; rerun with apply enabled' unless @operation + + apply_operation + end + + def apply_resolved! + raise MarketplaceError, 'Marketplace dependency plan was already applied' if applied? + raise MarketplaceError, 'Marketplace dependency preview expired; rerun with apply enabled' unless @operation + + apply_operation + end + + def apply_operation + @operation.call + @applied = true + self + end + + def expire! + @operation = nil unless applied? + self + end + end + + # Discovers dependencies from unresolved qualified references inside the + # package MPR, then verifies Content API candidates against the MPK module. + class DependencyResolver # rubocop:disable Metrics/ClassLength + PLATFORM_MODULES = %w[System].freeze + OFFICIAL_CONTENT_IDS = { + 'FeedbackModule' => 205_506, + 'DataWidgets' => 116_540, + 'Administration' => 23_513 + }.freeze + DATA_WIDGET_PATTERN = / + \Acom\.mendix\.widget\.web\. + (?:datagrid|datagriddatefilter|datagriddropdownfilter|datagridnumberfilter| + datagridtextfilter|dropdownsort|gallery|selectionhelper|treenode)\. + /ix + OFFICIAL_WIDGET_CONTENT_IDS = { + DATA_WIDGET_PATTERN => 116_540, + /\Acom\.mendix\.widget\.web\.combobox\.Combobox\z/i => 219_304 + }.freeze + + def initialize(target:, mpr:, installer:, api:, mendix_version:) + @target = File.expand_path(target) + @mpr = File.expand_path(mpr) + @installer = installer + @api = api + @mendix_version = mendix_version + @resolved = {} + @visited = {} + @blockers = [] + @project_modules = Mxrb.open(@mpr) { _1.modules.map(&:name) } + @required_widget_ids = widget_ids(@mpr) + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def resolve(identifier, apply: false, apply_resolved: false) + lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: @installer) + root, entry = lifecycle.installed(identifier) + Dir.mktmpdir('mxrb-marketplace-dependencies-') do |temporary| + @temporary = temporary + visit(root, safe_path(entry.fetch('archive'))) + dependencies = @resolved.values.freeze + widget_dependencies = resolve_widget_dependencies + plan = DependencyPlan.new( + root:, dependencies:, widget_dependencies:, blockers: @blockers.uniq + ) do + apply_dependencies(dependencies, widget_dependencies) + end + plan.apply! if apply + plan.apply_resolved! if apply_resolved + plan.expire! unless apply || apply_resolved + return plan + end + ensure + @temporary = nil + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + private + + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def visit(module_name, archive) + return if @visited[module_name] + + @visited[module_name] = true + dependency_names(archive).each do |dependency_name| + next if PLATFORM_MODULES.include?(dependency_name) || dependency_name == module_name + next if @project_modules.include?(dependency_name) && !packages.key?(dependency_name) + + dependency = installed_dependency(dependency_name) || resolve_dependency(dependency_name) + next unless dependency + + visit(dependency.module_name, dependency.archive) + @resolved[dependency.module_name] ||= dependency unless installed?(dependency.module_name) + end + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + def dependency_names(archive) + Dir.mktmpdir('mxrb-marketplace-dependency-model-') do |temporary| + project = extract_project(archive, temporary) + @required_widget_ids.concat(widget_ids(project)).uniq! + Mxrb.open(project) do |model| + return model.analyze.unresolved_references.filter_map do |reference| + reference.qualified_name.to_s.split(%r{[./]}, 2).first + end.uniq.sort + end + end + end + + def extract_project(archive_path, temporary) + Zip::File.open(archive_path) do |archive| + reader = ModulePackageReader.new(archive) + descriptor = reader.descriptor + reader.extract_project(descriptor, temporary) + end + rescue Zip::Error, REXML::ParseException => e + raise MarketplaceError, "invalid Mendix dependency package: #{e.message}" + end + + def installed_dependency(name) + pair = packages.find { |module_name, _entry| module_name.casecmp(name).zero? } + return unless pair + + module_name, entry = pair + Dependency.new(module_name, nil, safe_path(entry.fetch('archive')), dependency_names_for(entry)) + end + + def dependency_names_for(entry) + Array(entry['dependencies']) + end + + def resolve_dependency(name) # rubocop:disable Metrics/MethodLength + failures = [] + candidates(name).each do |content| + package = resolve_candidate(content) + next unless package + + archive = download_candidate(package) + inventory = ModulePackageInventory.read(archive) + next unless inventory.name == name + + dependencies = dependency_names(archive) + return Dependency.new(name, package, archive, dependencies) + rescue MarketplaceError => e + failures << "#{name}: candidate #{content['contentId']} failed: #{e.message}" + end + @blockers.concat(failures) + @blockers << "#{name}: no official Marketplace package matched the module identity" + nil + end + + def candidates(name) + (official_candidates(name) + queries(name).flat_map { @api.search(name: _1, limit: 100) }) + .select { Installer::IMPORTABLE_CONTENT_TYPES.include?(_1['type']) } + .uniq { _1['contentId'] } + end + + def official_candidates(name) + pair = OFFICIAL_CONTENT_IDS.find { |module_name, _content_id| module_name.casecmp(name).zero? } + pair ? [@api.content(pair.last)] : [] + end + + def queries(name) + humanized = name.gsub('_', ' ').gsub(/([a-z\d])([A-Z])/, '\\1 \\2') + [humanized, name].uniq + end + + def resolve_candidate(content) + @api.resolve(content.fetch('contentId'), mendix_version: @mendix_version) + end + + def download_candidate(package) + destination = File.join(@temporary, "#{package.content_id}-#{package.version_id}.mpk") + return destination if File.file?(destination) + + @api.download(package.version_id, destination, download_url: package.download_url) + end + + def resolve_widget_dependencies # rubocop:disable Metrics/MethodLength + missing = @required_widget_ids.reject { widget_installed?(_1) } + grouped = missing.group_by { official_widget_content_id(_1) } + Array(grouped.delete(nil)).each do |widget_id| + @blockers << "#{widget_id}: no verified official Marketplace widget mapping" + end + grouped.filter_map do |content_id, required_ids| + resolve_widget_dependency(content_id, required_ids) + rescue MarketplaceError => e + @blockers << "widget content #{content_id}: #{e.message}" + nil + end + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def resolve_widget_dependency(content_id, required_ids) + if package_installed?(content_id) + raise MarketplaceError, + "locked official package is present but does not provide #{required_ids.join(', ')}" + end + + existing = @resolved.values.find { _1.package&.content_id == content_id } + package = existing&.package || @api.resolve(content_id, mendix_version: @mendix_version) + @installer.validate_official_package!(package) + archive = existing&.archive || download_candidate(package) + inventory = WidgetBundleInventory.read(archive) + missing = required_ids - inventory.widget_ids + unless missing.empty? + raise MarketplaceError, + "downloaded package does not provide #{missing.join(', ')}" + end + + WidgetDependency.new(package, archive, inventory.widget_ids, required_ids.sort) + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + def official_widget_content_id(widget_id) + pair = OFFICIAL_WIDGET_CONTENT_IDS.find { |pattern, _content_id| pattern.match?(widget_id) } + pair&.last + end + + def widget_installed?(widget_id) + !Mxrb::WidgetPackage.find(@target, widget_id).nil? + end + + def widget_ids(mpr_path) + Mxrb.open(mpr_path) do |model| + model.all_units.flat_map do |unit| + collect_widget_ids(model.parse_bson(unit)) + end.uniq.sort + end + end + + def collect_widget_ids(value) + case value + when Hash + own = value['WidgetId'].to_s + nested = value.values.flat_map { collect_widget_ids(_1) } + own.empty? ? nested : [own] + nested + when Array + value.flat_map { collect_widget_ids(_1) } + else + [] + end + end + + def apply_dependencies(dependencies, widget_dependencies) # rubocop:disable Metrics/MethodLength + paths = rollback_paths(dependencies, widget_dependencies) + lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: @installer) + lifecycle.send(:with_rollback, paths) do + dependencies.each do |dependency| + @installer.send(:import_module, dependency.archive, dependency.package, @mpr) + end + widget_dependencies.each do |dependency| + next if package_installed?(dependency.package.content_id) + + @installer.send(:install_official_archive, dependency.archive, dependency.package, @mpr) + end + end + end + + def rollback_paths(dependencies, widget_dependencies) + archives = (dependencies + widget_dependencies).map(&:archive) + assets = archives.flat_map { package_asset_paths(_1) } + [ + @mpr, File.join(File.dirname(@mpr), 'mprcontents'), + File.join(@target, '.mxrb', 'marketplace.lock.json'), + File.join(@target, '.mxrb', 'marketplace'), + File.join(@target, '.mxrb', 'marketplace-originals'), + File.join(@target, 'theme', 'web', 'custom-variables.scss') + ] + assets.map { safe_path(_1) } + end + + def package_asset_paths(archive) + return [File.join('widgets', WidgetPackageInventory.read(archive).project_filename)] \ + if PackageEnvelope.kind(archive) == :widget + + ModulePackageInventory.read(archive).files.keys + end + + def package_installed?(content_id) + packages.any? { |_name, entry| entry['content_id'].to_s == content_id.to_s } + end + + def packages = OfficialMarketplace.lock(@target).fetch('packages') + def installed?(name) = packages.key?(name) + + def safe_path(relative) + path = File.expand_path(File.join(@target, relative.to_s)) + prefix = "#{@target}#{File::SEPARATOR}" + raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix) + + path + end + end + end +end diff --git a/lib/mxrb/official_marketplace/lifecycle.rb b/lib/mxrb/official_marketplace/lifecycle.rb new file mode 100644 index 0000000..0cbee9f --- /dev/null +++ b/lib/mxrb/official_marketplace/lifecycle.rb @@ -0,0 +1,367 @@ +# frozen_string_literal: true + +module Mxrb + module OfficialMarketplace + # Immutable preview whose mutation is guarded by explicit apply and blockers. + class LifecyclePlan + attr_reader :action, :name, :installed_version, :target_version, :changes, :blockers + + # rubocop:disable Metrics/ParameterLists + def initialize(action:, name:, installed_version:, target_version:, changes:, blockers:, &operation) + @action = action + @name = name + @installed_version = installed_version + @target_version = target_version + @changes = changes.freeze + @blockers = blockers.freeze + @operation = operation + @applied = false + end + # rubocop:enable Metrics/ParameterLists + + def safe? = blockers.empty? + def applied? = @applied + + def apply! + raise MarketplaceError, 'Marketplace lifecycle plan was already applied' if applied? + raise MarketplaceError, "Marketplace lifecycle plan is blocked: #{blockers.join('; ')}" unless safe? + + @operation.call + @applied = true + self + end + end + + # Read-only view of a module package used to compare update boundaries. + class ModulePackageInventory + attr_reader :name, :version, :module_id, :unit_ids, :files + + def initialize(name:, version:, module_id:, unit_ids:, files:) + @name = name + @version = version + @module_id = module_id + @unit_ids = unit_ids.freeze + @files = files.freeze + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def self.read(path) + Dir.mktmpdir('mxrb-marketplace-inspect-') do |temporary| + Zip::File.open(path) do |archive| + reader = ModulePackageReader.new(archive) + descriptor = reader.descriptor + source_path = reader.extract_project(descriptor, temporary) + staged = reader.stage_files(descriptor.files, temporary) + source = IO::MprFile.open(source_path, readonly: true) + module_unit = find_module(source, descriptor.name) + raise MarketplaceError, "module #{descriptor.name.inspect} is absent from package MPR" unless module_unit + + ids = subtree_ids(source, module_unit.fetch('UnitID')) + files = staged.transform_values { Digest::SHA256.file(_1).hexdigest } + return new( + name: descriptor.name, version: descriptor.version, + module_id: module_unit.fetch('UnitID'), unit_ids: ids, files: + ) + ensure + source&.close + end + end + rescue Zip::Error, REXML::ParseException => e + raise MarketplaceError, "invalid Mendix module package: #{e.message}" + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + def self.find_module(mpr, name) + mpr.units_by_containment('Modules').find { mpr.parse_contents(_1)['Name'] == name } + end + + def self.subtree_ids(mpr, root_id) + ids = [root_id] + loop do + children = mpr.all_units.filter_map do |unit| + unit.fetch('UnitID') if ids.include?(unit['ContainerID']) && !ids.include?(unit.fetch('UnitID')) + end + break if children.empty? + + ids.concat(children) + end + ids + end + end + + # Transactional lifecycle for modules already recorded in marketplace.lock.json. + class Lifecycle # rubocop:disable Metrics/ClassLength + def initialize(target:, mpr:, installer:) + @target = File.expand_path(target) + @mpr = mpr && File.expand_path(mpr) + @installer = installer + end + + def installed(identifier) + pair = packages.find do |name, entry| + name.casecmp(identifier.to_s).zero? || entry['content_id'].to_s == identifier.to_s + end + raise MarketplaceError, "Marketplace package #{identifier.inspect} is not installed" unless pair + raise MarketplaceError, "Marketplace package #{pair.first.inspect} is not an imported module" \ + unless pair.last['kind'] == 'module' + + pair + end + + def mpr_path(entry) + path = safe_path(entry.fetch('destination')) + raise MarketplaceError, 'selected MPR does not match the Marketplace lock' if @mpr && @mpr != path + + path + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def plan_remove(identifier) + name, entry = installed(identifier) + mpr = mpr_path(entry) + ids = target_unit_ids(mpr, entry) + inventory = cached_inventory(entry) + shared = shared_files(name) + blockers = base_blockers(entry, inventory, ids) + blockers << "locked module name does not match cached package #{inventory.name}" if inventory.name != name + blockers.concat(external_reference_blockers(mpr, ids)) + blockers.concat(modified_asset_blockers(inventory, name)) + removable = inventory.files.keys - shared + changes = ["delete #{ids.size} MPR units", "delete #{removable.size} package assets", + 'delete cached package and lock entry'] + LifecyclePlan.new( + action: :remove, name:, installed_version: entry['version'], target_version: nil, + changes:, blockers: + ) { apply_remove(name, entry, mpr, ids, removable) } + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + def plan_update(identifier, archive, package) + name, entry = installed(identifier) + mpr = mpr_path(entry) + current_ids = target_unit_ids(mpr, entry) + current = cached_inventory(entry) + replacement = ModulePackageInventory.read(archive) + missing_ids = current_ids - replacement.unit_ids + blockers = base_blockers(entry, current, current_ids) + blockers << "replacement module name changed from #{name} to #{replacement.name}" if replacement.name != name + blockers << 'replacement module identity changed' if replacement.module_id != entry['module_id'] + blockers << "downloaded package version #{replacement.version} does not match #{package.version}" \ + unless package_version_matches?(replacement.version, package.version) + blockers << "version #{package.version} is already installed" if package.version.to_s == entry['version'].to_s + blockers.concat(external_reference_blockers(mpr, missing_ids)) + blockers.concat(modified_asset_blockers(current, name)) + obsolete = current.files.keys - replacement.files.keys - shared_files(name) + changes = ["replace #{current_ids.size} MPR units with #{replacement.unit_ids.size}", + "install #{replacement.files.size} package assets", + "delete #{obsolete.size} obsolete package assets", 'replace cache and lock metadata'] + LifecyclePlan.new( + action: :update, name:, installed_version: entry['version'], target_version: package.version, + changes:, blockers: + ) { apply_update(entry, mpr, current_ids, obsolete, archive, package, replacement) } + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + private + + def packages = OfficialMarketplace.lock(@target).fetch('packages') + + def safe_path(relative) + path = File.expand_path(File.join(@target, relative.to_s)) + prefix = "#{@target}#{File::SEPARATOR}" + raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix) + + path + end + + def cached_inventory(entry) + archive = safe_path(entry.fetch('archive')) + raise MarketplaceError, "cached Marketplace package is missing: #{archive}" unless File.file?(archive) + + ModulePackageInventory.read(archive) + end + + def target_unit_ids(mpr_path, entry) + raise MarketplaceError, "MPR not found: #{mpr_path}" unless File.file?(mpr_path) + + mpr = IO::MprFile.open(mpr_path, readonly: true) + root = mpr.unit(entry.fetch('module_id')) + raise MarketplaceError, "module #{entry.fetch('module_id')} is absent from target MPR" unless root + + ModulePackageInventory.subtree_ids(mpr, root.fetch('UnitID')) + ensure + mpr&.close + end + + def base_blockers(entry, inventory, ids) + blockers = [] + blockers << 'locked module identity does not match cached package' if inventory.module_id != entry['module_id'] + blockers << 'locked unit count does not match target module tree' if entry['units'].to_i != ids.size + blockers + end + + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + def external_reference_blockers(mpr_path, ids) + return [] if ids.empty? + + id_set = ids.map(&:downcase) + mpr = IO::MprFile.open(mpr_path, readonly: true) + mpr.all_units.filter_map do |unit| + next if id_set.include?(unit.fetch('UnitID').downcase) + + document = mpr.parse_contents(unit) + matches = id_set.select { JSON.generate(document).downcase.include?(_1) } + next if matches.empty? + + "#{document['Name'] || document['$Type'] || unit.fetch('UnitID')} references " \ + "#{matches.size} unit(s) scheduled for removal" + end + ensure + mpr&.close + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + + def modified_asset_blockers(inventory, owner_name) + shared = shared_files(owner_name) + inventory.files.filter_map do |relative, expected| + next if shared.include?(relative) + + path = safe_path(relative) + actual = Digest::SHA256.file(path).hexdigest if File.file?(path) && !File.symlink?(path) + "package asset changed or missing: #{relative}" unless actual == expected + end + end + + def shared_files(name) + packages.filter_map do |other_name, entry| + Array(entry['files']) unless other_name == name + end.flatten.uniq + end + + def package_version_matches?(manifest_version, official_version) + value = manifest_version.to_s + value.empty? || value == 'unknown' || value == official_version.to_s + end + + def apply_remove(name, entry, mpr, ids, files) + paths = lifecycle_paths(entry, mpr, files) + with_rollback(paths) do + delete_units(mpr, ids) + restore_assets(entry, files) + remove_atlas_variables if name == 'Atlas_Core' + FileUtils.rm_f(safe_path(entry.fetch('archive'))) + delete_asset_backups(entry) + delete_lock_entry(name) + end + end + + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + def apply_update(entry, mpr, ids, obsolete, archive, package, replacement) + new_cache = cache_path(replacement) + files = (Array(entry['files']) + replacement.files.keys).uniq + paths = lifecycle_paths(entry, mpr, files) + [new_cache] + with_rollback(paths.uniq) do + delete_units(mpr, ids) + @installer.send(:import_module, archive, package, mpr) + restore_assets(entry, obsolete) + delete_asset_backups(entry, obsolete) + old_cache = safe_path(entry.fetch('archive')) + FileUtils.rm_f(old_cache) unless old_cache == new_cache + end + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists + + def delete_units(mpr_path, ids) + mpr = IO::MprFile.open(mpr_path) + mpr.transaction { ids.reverse_each { mpr.delete_unit(_1) } } + ensure + mpr&.close + end + + def lifecycle_paths(entry, mpr, files) + contents = File.join(File.dirname(mpr), 'mprcontents') + lock = File.join(@target, '.mxrb', 'marketplace.lock.json') + custom = File.join(@target, 'theme', 'web', 'custom-variables.scss') + originals = File.join(@target, '.mxrb', 'marketplace-originals') + [mpr, contents, lock, custom, originals, safe_path(entry.fetch('archive'))] + files.map { safe_path(_1) } + end + + def cache_path(inventory) + version = inventory.version.to_s.gsub(/[^A-Za-z0-9_.-]/, '_') + File.join(@target, '.mxrb', 'marketplace', "#{inventory.name}-#{version}.mpk") + end + + def with_rollback(paths) + Dir.mktmpdir('mxrb-marketplace-rollback-') do |temporary| + snapshots = [] + paths.uniq.each_with_index { |path, index| snapshots << snapshot(path, temporary, index) } + yield + rescue StandardError + snapshots.reverse_each { restore_snapshot(_1) } + raise + end + end + + def snapshot(path, temporary, index) + backup = File.join(temporary, index.to_s) + type = snapshot_type(path) + FileUtils.cp_r(path, backup) if type == :directory + FileUtils.cp(path, backup) if type == :file + [path, backup, type] + end + + def snapshot_type(path) + return :directory if File.directory?(path) + return :file if File.file?(path) + + :missing + end + + def restore_snapshot(snapshot) + path, backup, type = snapshot + FileUtils.rm_rf(path) if File.directory?(path) + FileUtils.rm_f(path) if File.file?(path) + return if type == :missing + + FileUtils.mkdir_p(File.dirname(path)) + type == :directory ? FileUtils.cp_r(backup, path) : FileUtils.cp(backup, path) + end + + def delete_lock_entry(name) + path = File.join(@target, '.mxrb', 'marketplace.lock.json') + lock = OfficialMarketplace.lock(@target) + lock.fetch('packages').delete(name) + @installer.send(:write_lock_atomically, path, lock) + end + + def restore_assets(entry, files) # rubocop:disable Metrics/MethodLength + originals = entry.fetch('asset_originals', {}) + files.each do |relative| + destination = safe_path(relative) + backup = originals[relative] + if backup && File.file?(safe_path(backup)) + FileUtils.mkdir_p(File.dirname(destination)) + FileUtils.cp(safe_path(backup), destination) + else + FileUtils.rm_f(destination) + end + end + end + + def delete_asset_backups(entry, files = nil) + originals = entry.fetch('asset_originals', {}) + selected = files ? originals.slice(*files) : originals + selected.each_value { FileUtils.rm_f(safe_path(_1)) if _1 } + end + + def remove_atlas_variables + path = File.join(@target, 'theme', 'web', 'custom-variables.scss') + return unless File.file?(path) + + lines = File.readlines(path).reject { _1.strip == Installer::ATLAS_VARIABLES_IMPORT } + File.write(path, lines.join) + end + end + end +end diff --git a/lib/mxrb/official_marketplace/module_package_importer.rb b/lib/mxrb/official_marketplace/module_package_importer.rb index e9a0d69..325e057 100644 --- a/lib/mxrb/official_marketplace/module_package_importer.rb +++ b/lib/mxrb/official_marketplace/module_package_importer.rb @@ -119,8 +119,10 @@ def initialize(target_root, temporary) @changes = [] end - def install(staged_files) - staged_files.each { |relative, source| install_file(relative, source) } + def install(staged_files, protected_files = []) + staged_files.each do |relative, source| + install_file(relative, source) unless protected_files.include?(relative) + end end def rollback @@ -133,6 +135,7 @@ def rollback def install_file(relative, source) destination = safe_destination(relative) + raise MarketplaceError, "asset destination is a symbolic link: #{relative}" if File.symlink?(destination) raise MarketplaceError, "asset destination is a directory: #{relative}" if File.directory?(destination) backup = backup_file(relative, destination) @@ -166,11 +169,13 @@ class ModulePackageImporter # rubocop:disable Metrics/ClassLength :source_version, :target_version ) - def initialize(package_path, mpr_path, target_root: nil, allow_model_upgrade: false) + def initialize(package_path, mpr_path, target_root: nil, allow_model_upgrade: false, + protected_files: []) @package_path = File.expand_path(package_path) @mpr_path = File.expand_path(mpr_path) @target_root = File.expand_path(target_root || File.dirname(@mpr_path)) @allow_model_upgrade = allow_model_upgrade + @protected_files = protected_files end def import! @@ -221,7 +226,7 @@ def import_transaction(source, target, descriptor, staged_files, assets, importe validate_target!(target, descriptor.name, units) target.transaction do imported_ids.concat(insert_units(source, target, module_unit, units)) - assets.install(staged_files) + assets.install(staged_files, @protected_files) end end # rubocop:enable Metrics/ParameterLists diff --git a/lib/mxrb/official_marketplace/widget_package_installer.rb b/lib/mxrb/official_marketplace/widget_package_installer.rb new file mode 100644 index 0000000..b1ff61e --- /dev/null +++ b/lib/mxrb/official_marketplace/widget_package_installer.rb @@ -0,0 +1,414 @@ +# frozen_string_literal: true + +require 'digest' +require 'fileutils' +require 'json' +require 'rexml/document' +require 'zip' + +module Mxrb + module OfficialMarketplace + WidgetInstallation = Data.define( + :package, :destination, :sha256, :widget_name, :widget_ids, :archive + ) + + # Validates the two official Marketplace package envelopes MXRB accepts. + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + class PackageEnvelope + def self.kind(path) + Zip::File.open(path) do |archive| + package = archive.find_entry('package.xml') + raise MarketplaceError, 'package.xml is missing' unless package + + document = REXML::Document.new(package.get_input_stream.read) + names = document.root&.elements&.map(&:name) || [] + return :module if names.include?('modelerProject') + return :widget if names.include?('clientModule') + + raise MarketplaceError, 'package.xml does not declare a module or widget' + end + rescue Zip::Error, REXML::ParseException => e + raise MarketplaceError, "invalid Mendix package: #{e.message}" + end + end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity + + # Read-only, fail-closed inventory for a clientModule MPK. + # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength + class WidgetPackageInventory + attr_reader :name, :version, :widget_ids, :project_filename + + def initialize(name:, version:, widget_ids:, project_filename:) + @name = name + @version = version + @widget_ids = widget_ids.freeze + @project_filename = project_filename + end + + def self.read(path) + Zip::File.open(path) do |archive| + validate_entries!(archive) + document = package_document(archive) + clients = elements(document.root, 'clientModule') + raise MarketplaceError, 'package.xml must declare exactly one clientModule' unless clients.one? + + client = clients.first + + name = valid_name(client.attributes['name']) + version = valid_version(client.attributes['version']) + widget_paths = elements(client, 'widgetFile').map do |element| + safe_relative_path(element.attributes['path']) + end.uniq + raise MarketplaceError, 'widget package does not declare widgetFiles' if widget_paths.empty? + + validate_declared_files!(archive, client) + widget_ids = widget_paths.map { widget_id(archive, _1) } + filename = project_filename(name, widget_ids) + new(name:, version:, widget_ids:, project_filename: filename) + end + rescue Zip::Error, REXML::ParseException => e + raise MarketplaceError, "invalid Mendix widget package: #{e.message}" + end + + class << self + private + + def package_document(archive) + package = archive.find_entry('package.xml') + raise MarketplaceError, 'package.xml is missing' unless package + + REXML::Document.new(package.get_input_stream.read) + end + + def validate_entries!(archive) + normalized = {} + archive.each do |entry| + path = safe_relative_path(entry.name, directory: true) + key = path.downcase + raise MarketplaceError, "duplicate widget package path #{entry.name.inspect}" if normalized[key] + raise MarketplaceError, "symbolic link in widget package: #{entry.name}" if entry.symlink? + + normalized[key] = true + end + end + + def validate_declared_files!(archive, client) + elements(client, 'file').each do |element| + relative = safe_relative_path(element.attributes['path'], directory: true) + present = archive.any? do |entry| + normalized = entry.name.to_s.tr('\\', '/').delete_suffix('/') + normalized == relative || normalized.start_with?("#{relative}/") + end + raise MarketplaceError, "declared widget file is missing: #{relative}" unless present + end + end + + def widget_id(archive, relative) + entry = archive.find_entry(relative) || archive.find_entry(relative.tr('/', '\\')) + raise MarketplaceError, "declared widget definition is missing: #{relative}" unless entry&.file? + + root = REXML::Document.new(entry.get_input_stream.read).root + value = root&.name == 'widget' ? root.attributes['id'].to_s : '' + return value if value.match?(/\A[A-Za-z][A-Za-z0-9_]*(?:\.[A-Za-z][A-Za-z0-9_]*)+\z/) + + raise MarketplaceError, "invalid widget ID #{value.inspect} in #{relative}" + end + + def project_filename(name, widget_ids) + namespaces = widget_ids.map { _1.split('.')[0...-2] }.uniq + raise MarketplaceError, 'widget definitions do not share one package namespace' unless namespaces.one? + + "#{(namespaces.first + [name]).join('.')}.mpk" + end + + def valid_name(value) + name = value.to_s + return name if name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/) + + raise MarketplaceError, "invalid clientModule name #{name.inspect}" + end + + def valid_version(value) + version = value.to_s + return version if version.match?(/\A[0-9A-Za-z][0-9A-Za-z.+_-]*\z/) + + raise MarketplaceError, "invalid clientModule version #{version.inspect}" + end + + def safe_relative_path(value, directory: false) + path = value.to_s.tr('\\', '/').delete_suffix('/') + parts = path.split('/') + unsafe = path.empty? || path.start_with?('/') || parts.include?('..') || parts.include?('') || + path.include?("\0") + raise MarketplaceError, "unsafe widget package path #{value.inspect}" if unsafe + if !directory && File.extname(path).downcase != '.xml' + raise MarketplaceError, "widget definition is not XML: #{value.inspect}" + end + + path + end + + def elements(root, name) + found = [] + root&.each_recursive { found << _1 if _1.is_a?(REXML::Element) && _1.name == name } + found + end + end + end + # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength + + # Enumerates widget identities delivered either directly or as declared + # assets of a modelerProject package such as Data Widgets. + # rubocop:disable Metrics/AbcSize, Metrics/MethodLength + class WidgetBundleInventory + attr_reader :kind, :widget_ids + + def initialize(kind:, widget_ids:) + @kind = kind + @widget_ids = widget_ids.freeze + end + + def self.read(path) + kind = PackageEnvelope.kind(path) + return new(kind:, widget_ids: WidgetPackageInventory.read(path).widget_ids) if kind == :widget + + ModulePackageInventory.read(path) + ids = Dir.mktmpdir('mxrb-widget-bundle-') do |temporary| + Zip::File.open(path) do |archive| + reader = ModulePackageReader.new(archive) + descriptor = reader.descriptor + descriptor.files.grep(%r{\Awidgets/[^/]+\.mpk\z}i).flat_map do |relative| + entry = archive.find_entry(relative) || archive.find_entry(relative.tr('/', '\\')) + destination = File.join(temporary, File.basename(relative)) + File.open(destination, 'wb') { ::IO.copy_stream(entry.get_input_stream, _1) } + WidgetPackageInventory.read(destination).widget_ids + end + end + end + new(kind:, widget_ids: ids.uniq.sort) + rescue Zip::Error, REXML::ParseException => e + raise MarketplaceError, "invalid Mendix widget bundle: #{e.message}" + end + end + # rubocop:enable Metrics/AbcSize, Metrics/MethodLength + + # Installs an already authenticated official widget archive as one project-owned MPK. + # rubocop:disable Metrics + class WidgetPackageInstaller + def initialize(target:) + @target = File.expand_path(target) + end + + def install(archive, package) + validate_target! + inventory = WidgetPackageInventory.read(archive) + validate_identity!(inventory, package) + digest = Digest::SHA256.file(archive).hexdigest + destination = safe_path(File.join('widgets', inventory.project_filename)) + cache = safe_path(cache_relative(inventory, package)) + lock_path = safe_path(File.join('.mxrb', 'marketplace.lock.json')) + current = validate_boundaries!(package, destination, cache, lock_path) + install_transaction( + archive, package, inventory, digest, destination, cache, lock_path, current + ) + end + + private + + def validate_target! + raise MarketplaceError, "Marketplace target not found: #{@target}" unless File.directory?(@target) + raise MarketplaceError, "Marketplace target is a symbolic link: #{@target}" if File.symlink?(@target) + end + + def validate_identity!(inventory, package) + return if inventory.version == package.version.to_s + + raise MarketplaceError, + "widget package version #{inventory.version} does not match Marketplace #{package.version}" + end + + def validate_boundaries!(package, destination, cache, lock_path) + ensure_no_symlink_components(destination) + ensure_no_symlink_components(cache) + ensure_no_symlink_components(lock_path) + raise MarketplaceError, "widget destination is a symbolic link: #{destination}" if File.symlink?(destination) + raise MarketplaceError, "widget destination is a directory: #{destination}" if File.directory?(destination) + + lock = OfficialMarketplace.lock(@target) + current = lock.fetch('packages').find do |name, entry| + name.casecmp(package.name.to_s).zero? || + entry['content_id'].to_s == package.content_id.to_s + end + validate_current!(current, package, destination) + validate_asset_owners!(lock, destination) unless current + unless !File.exist?(cache) || current&.last&.[]('archive') == relative(cache) + raise MarketplaceError, "widget cache destination already exists: #{cache}" + end + + current + end + + def validate_current!(current, package, destination) + return unless current + + name, entry = current + raise MarketplaceError, "Marketplace package #{name.inspect} is not a widget" unless entry['kind'] == 'widget' + unless entry['destination'] == relative(destination) + raise MarketplaceError, "Marketplace widget #{name.inspect} changed its destination" + end + unless OfficialMarketplace.verify_widget(@target, entry)[:valid] + raise MarketplaceError, "installed Marketplace widget #{name.inspect} failed verification" + end + return unless entry['version'].to_s == package.version.to_s + + raise MarketplaceError, "widget version #{package.version} is already installed" + end + + def validate_asset_owners!(lock, destination) + relative_path = relative(destination) + owners = lock.fetch('packages').select do |_name, entry| + Array(entry['files']).include?(relative_path) + end + return if owners.empty? || !File.file?(destination) + + expected = owners.filter_map { |_name, entry| locked_asset_digest(entry, relative_path) }.uniq + actual = Digest::SHA256.file(destination).hexdigest + return if expected.include?(actual) + + raise MarketplaceError, "owned widget asset changed: #{relative_path}" + end + + def locked_asset_digest(entry, relative_path) + return entry['sha256'] if entry['kind'] == 'widget' && entry['destination'] == relative_path + return unless entry['kind'] == 'module' + + archive = safe_path(entry.fetch('archive')) + raise MarketplaceError, "cached Marketplace package is missing: #{archive}" unless File.file?(archive) + + ModulePackageInventory.read(archive).files[relative_path] + end + + def install_transaction(archive, package, inventory, digest, destination, cache, lock_path, current) + original = current&.last&.[]('asset_original') + backup = original ? safe_path(original) : backup_path(package, destination) + old_cache = safe_path(current.last.fetch('archive')) if current + paths = [destination, cache, lock_path, backup, old_cache].compact.uniq + with_rollback(paths) do + preserve_original(destination, backup) unless original + atomic_copy(archive, destination) + atomic_copy(archive, cache) + verify_copy!(destination, digest) + verify_copy!(cache, digest) + write_lock( + lock_path, package, inventory, digest, destination, cache, + asset_original: File.file?(backup) ? relative(backup) : nil, + replaced_name: current&.first + ) + FileUtils.rm_f(old_cache) if old_cache && old_cache != cache + end + WidgetInstallation.new(package, destination, digest, inventory.name, inventory.widget_ids, cache) + end + + def preserve_original(destination, backup) + return unless File.file?(destination) + + FileUtils.mkdir_p(File.dirname(backup)) + FileUtils.cp(destination, backup) + end + + def backup_path(package, destination) + owner = "Widget-#{package.content_id || package.name.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')}" + safe_path(File.join('.mxrb', 'marketplace-originals', owner, relative(destination))) + end + + def atomic_copy(source, destination) + temporary = "#{destination}.tmp-#{Process.pid}" + FileUtils.mkdir_p(File.dirname(destination)) + FileUtils.cp(source, temporary) + File.rename(temporary, destination) + ensure + FileUtils.rm_f(temporary) + end + + def verify_copy!(path, digest) + return if Digest::SHA256.file(path).hexdigest == digest + + raise MarketplaceError, "widget package checksum mismatch: #{path}" + end + + def write_lock(path, package, inventory, digest, destination, cache, + asset_original:, replaced_name: nil) + lock = OfficialMarketplace.lock(@target) + lock.fetch('packages').delete(replaced_name) if replaced_name && replaced_name != package.name + lock.fetch('packages')[package.name] = { + 'kind' => 'widget', 'version' => package.version, 'source' => package.source, + 'repository' => package.repository, 'sha256' => digest, + 'destination' => relative(destination), 'archive' => relative(cache), + 'files' => [relative(destination)], 'asset_original' => asset_original, + 'widget_name' => inventory.name, 'widget_ids' => inventory.widget_ids, + 'content_id' => package.content_id, 'version_id' => package.version_id, + 'content_type' => package.content_type, 'version_type' => package.version_type, + 'security_issues' => package.security_issues, 'private' => package.private, + 'company_approved' => package.company_approved + }.compact + temporary = "#{path}.tmp-#{Process.pid}" + FileUtils.mkdir_p(File.dirname(path)) + File.write(temporary, JSON.pretty_generate(lock) << "\n") + File.rename(temporary, path) + ensure + FileUtils.rm_f(temporary) + end + + def with_rollback(paths) + Dir.mktmpdir('mxrb-widget-rollback-') do |temporary| + snapshots = paths.each_with_index.map { |path, index| snapshot(path, temporary, index) } + yield + rescue StandardError + snapshots.reverse_each { restore_snapshot(_1) } + raise + end + end + + def snapshot(path, temporary, index) + backup = File.join(temporary, index.to_s) + type = File.file?(path) ? :file : :missing + FileUtils.cp(path, backup) if type == :file + [path, backup, type] + end + + def restore_snapshot(snapshot) + path, backup, type = snapshot + FileUtils.rm_f(path) + return if type == :missing + + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.cp(backup, path) + end + + def cache_relative(inventory, package) + version = package.version.to_s.gsub(/[^A-Za-z0-9_.-]/, '_') + stem = File.basename(inventory.project_filename, '.mpk') + File.join('.mxrb', 'marketplace', "#{stem}-#{version}.mpk") + end + + def safe_path(relative) + path = File.expand_path(File.join(@target, relative)) + prefix = "#{@target}#{File::SEPARATOR}" + raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix) + + path + end + + def relative(path) = path.delete_prefix("#{@target}#{File::SEPARATOR}") + + def ensure_no_symlink_components(path) + current = @target + relative(path).split(File::SEPARATOR)[0...-1].each do |part| + current = File.join(current, part) + raise MarketplaceError, "widget destination traverses a symbolic link: #{current}" if File.symlink?(current) + end + end + end + # rubocop:enable Metrics + end +end diff --git a/lib/mxrb/oql/index_advisor.rb b/lib/mxrb/oql/index_advisor.rb index 20b4c24..ab28bd6 100644 --- a/lib/mxrb/oql/index_advisor.rb +++ b/lib/mxrb/oql/index_advisor.rb @@ -30,7 +30,7 @@ def collect_evidence(queries) def pressured_relations(report) report.findings.select { _1.rule == :table_sequential_pressure } - .map { _1.subject.to_s.split('.').last.downcase } + .map { _1.subject.to_s.split('.').last.downcase } end def candidates_for(evidence, pressured) diff --git a/lib/mxrb/protocols.rb b/lib/mxrb/protocols.rb index 33e8334..f03d380 100644 --- a/lib/mxrb/protocols.rb +++ b/lib/mxrb/protocols.rb @@ -16,7 +16,7 @@ module Protocols # verified fixture provides it; GUID-based detection then fails closed. Entry = Data.define( :protocol, :name, :publisher, :category, :marketplace_id, - :appstore_guids, :source_url, :evidence_date + :content_type, :appstore_guids, :source_url, :evidence_date ) do def matches_guid?(guid) key = guid.to_s @@ -24,25 +24,45 @@ def matches_guid?(guid) end end - # Verified from official Mendix pages on 2026-08-02. + # Verified from official Mendix Marketplace pages. Public component ids are + # useful for planning; module recognition remains GUID-only and fail-closed. REGISTRY = [ Entry.new( protocol: :mqtt, name: 'MQTT', publisher: 'Mendix', category: 'Messaging', - marketplace_id: '119508', appstore_guids: [].freeze, + marketplace_id: '119508', content_type: 'Service', appstore_guids: [].freeze, source_url: 'https://marketplace.mendix.com/link/component/119508', - evidence_date: '2026-08-02' + evidence_date: '2026-08-04' ), Entry.new( protocol: :opc_ua, name: 'OPC-UA Connector', publisher: 'Mendix', category: 'Industrial', - marketplace_id: '230843', appstore_guids: [].freeze, + marketplace_id: '230843', content_type: 'Module', appstore_guids: [].freeze, source_url: 'https://marketplace.mendix.com/link/component/230843', - evidence_date: '2026-08-02' + evidence_date: '2026-08-04' ), Entry.new( protocol: :opc_ua, name: 'OPC UA Client Connector', publisher: 'Mendix', category: 'Industrial', - marketplace_id: '117391', appstore_guids: [].freeze, + marketplace_id: '117391', content_type: 'Service', appstore_guids: [].freeze, source_url: 'https://marketplace.mendix.com/link/component/117391', - evidence_date: '2026-08-02' + evidence_date: '2026-08-04' + ), + Entry.new( + protocol: :kafka, name: 'Kafka', publisher: 'Mendix', category: 'Messaging', + marketplace_id: '105878', content_type: 'Module', appstore_guids: [].freeze, + source_url: 'https://marketplace.mendix.com/link/component/105878', + evidence_date: '2026-08-04' + ), + Entry.new( + protocol: :websocket, name: 'WebsocketClient', publisher: 'Mendix', category: 'Communication', + marketplace_id: '235426', content_type: 'Module', appstore_guids: [].freeze, + source_url: 'https://marketplace.mendix.com/link/component/235426', + evidence_date: '2026-08-04' + ), + Entry.new( + protocol: :amqp, name: 'eMagiz Mendix Connector (Legacy)', publisher: 'eMagiz', + category: 'Communication', marketplace_id: '118800', content_type: 'Service', + appstore_guids: [].freeze, + source_url: 'https://marketplace.mendix.com/link/component/118800', + evidence_date: '2026-08-04' ) ].freeze @@ -54,7 +74,7 @@ def matches_guid?(guid) def all = REGISTRY def find_by_protocol(protocol) - key = protocol.to_s.downcase.to_sym + key = normalize_protocol(protocol) REGISTRY.select { _1.protocol == key } end @@ -93,5 +113,11 @@ def connector_for(mod, entry) end def surface_names(items) = items.map(&:name).compact.sort + + def normalize_protocol(protocol) + protocol.to_s.strip.downcase.tr('- ', '__').to_sym + end end end + +require_relative 'protocols/plan' diff --git a/lib/mxrb/protocols/plan.rb b/lib/mxrb/protocols/plan.rb new file mode 100644 index 0000000..4b29420 --- /dev/null +++ b/lib/mxrb/protocols/plan.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module Mxrb + # Preview/apply orchestration for verified protocol connector packages. + module Protocols + Request = Data.define(:protocol, :version, :marketplace_id) + Adapter = Data.define(:installer, :api) + + # Preview/apply boundary for a real official Marketplace connector. Planning + # never writes. Apply is available only when an authenticated Content API and + # an official installer were explicitly supplied by the caller. + class Plan + attr_reader :request, :entry, :mendix_version, :package + + def initialize(request:, entry:, context:) + @request = request + @entry = entry + @mendix_version = context.fetch(:mendix_version).to_s + @adapter = context[:adapter] + @package = context[:package] + @applied = false + end + + def changes + [{ + action: :install, protocol: entry.protocol, name: entry.name, + marketplace_id: entry.marketplace_id, + version: package&.version || request.version || 'latest-compatible', + mendix_version:, source_url: entry.source_url + }.freeze].freeze + end + + def blocked_reasons + [].tap do |reasons| + reasons << 'official Marketplace adapter was not provided' unless @adapter + reasons << 'official Marketplace version was not resolved' unless package + end.freeze + end + + def safe? = blocked_reasons.empty? + def applied? = @applied + + def apply! + raise MarketplaceError, "connector plan is blocked: #{blocked_reasons.join('; ')}" unless safe? + raise MarketplaceError, 'connector plan was already applied' if applied? + + result = @adapter.installer.pull_official( + entry.marketplace_id, version: request.version, mendix_version:, api: @adapter.api + ) + @applied = true + result + end + end + + module_function + + def request(protocol, version: nil, marketplace_id: nil) + Request.new( + protocol: normalize_protocol(protocol), version: version&.to_s, + marketplace_id: marketplace_id&.to_s + ) + end + + def adapter(installer:, api:) = Adapter.new(installer:, api:) + + def plan(protocol, mendix_version:, version: nil, marketplace_id: nil, adapter: nil) + connector_request = request(protocol, version:, marketplace_id:) + entry = planning_entry(connector_request) + package = adapter&.api&.resolve( + entry.marketplace_id, version: connector_request.version, mendix_version: + ) + Plan.new( + request: connector_request, entry:, + context: { mendix_version:, adapter:, package: } + ) + end + + def planning_entry(request) + candidates = find_by_protocol(request.protocol) + candidates = candidates.select { _1.marketplace_id == request.marketplace_id } if request.marketplace_id + raise MarketplaceError, "no verified Marketplace connector for #{request.protocol.inspect}" if candidates.empty? + + candidates.find { _1.content_type == 'Module' } || candidates.first + end + end +end diff --git a/lib/mxrb/runtime/native.rb b/lib/mxrb/runtime/native.rb index d2afb20..77cf196 100644 --- a/lib/mxrb/runtime/native.rb +++ b/lib/mxrb/runtime/native.rb @@ -186,7 +186,7 @@ def count(entity, xpath = nil) def execute(flow, variables) objects = flow.objects.to_h { [identifier(_1), _1] } outgoing = flow.flows.reject { _1['IsErrorHandler'] == true } - .group_by { identifier(_1['OriginPointer']) } + .group_by { identifier(_1['OriginPointer']) } current = flow.objects.find { _1['$Type'] == 'Microflows$StartEvent' } raise NativeRuntimeError, "microflow #{flow.name} has no start event" unless current diff --git a/lib/mxrb/runtime/toolchain.rb b/lib/mxrb/runtime/toolchain.rb index 1b97cdd..5330673 100644 --- a/lib/mxrb/runtime/toolchain.rb +++ b/lib/mxrb/runtime/toolchain.rb @@ -2,9 +2,7 @@ module Mxrb module Runtime - RUNTIME_REQUIRED_FILES = %w[ - launcher/runtimelauncher.jar pad/bin/start.hbs pad/etc/example.conf pad/etc/variables.conf - ].freeze + RUNTIME_REQUIRED_FILES = %w[launcher/runtimelauncher.jar].freeze Plan = Data.define( :mendix_version, :java_version, :toolchain_path, diff --git a/lib/mxrb/semantic/onnx_embedder.rb b/lib/mxrb/semantic/onnx_embedder.rb index dd13e9d..8a483ba 100644 --- a/lib/mxrb/semantic/onnx_embedder.rb +++ b/lib/mxrb/semantic/onnx_embedder.rb @@ -5,6 +5,7 @@ module Semantic # Optional sentence-transformer backend powered by the informers gem. class OnnxEmbedder MODEL = 'sentence-transformers/all-MiniLM-L6-v2' + MODEL_REVISION = '1110a243fdf4706b3f48f1d95db1a4f5529b4d41' def self.available? require 'informers' @@ -16,7 +17,7 @@ def self.available? def initialize(pipeline: nil) require 'informers' unless pipeline @pipeline = pipeline || Informers.pipeline( - 'embedding', MODEL + 'embedding', MODEL, revision: MODEL_REVISION ) @dimension = nil end diff --git a/lib/mxrb/templates/project/10.24.0.73019.json b/lib/mxrb/templates/project/10.24.0.73019.json new file mode 100644 index 0000000..0e8b969 --- /dev/null +++ b/lib/mxrb/templates/project/10.24.0.73019.json @@ -0,0 +1,32 @@ +{ + "format_version": "v1", + "units": [ + { + "unit_id": "4e70063d-53e1-477a-ab92-6b763330c293", + "container_id": "80748544-417e-434a-a27a-354ba38115de", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Settings$ProjectSettings", + "contents": "gwsAAAUkSUQAEAAAAAA9BnBO4VN6R6uSa3YzMMKTAiRUeXBlABkAAABTZXR0aW5ncyRQcm9qZWN0U2V0dGluZ3MABFNldHRpbmdzADYLAAAQMAACAAAAAzEA7AAAAAUkSUQAEAAAAAC82XyoYWY3WqFPOBeQlP/sAiRUeXBlAB8AAABGb3JtcyRXZWJVSVByb2plY3RTZXR0aW5nc1BhcnQACEVuYWJsZU1pY3JvZmxvd1JlYWNoYWJpbGl0eUFuYWx5c2lzAAEIRW5hYmxlTmV3V2lkZ2V0R2VuZXJhdGlvbgABCEVuYWJsZVdpZGdldEJ1bmRsaW5nAAAEVGhlbWVNb2R1bGVPcmRlcgAMAAAAEDAAAwAAAAACVXJsUHJlZml4AAIAAABwAAJVc2VPcHRpbWl6ZWRDbGllbnQAAwAAAE5vAAADMgBuAAAABSRJRAAQAAAAACCuBxzrK2RQpMT8mGbKGecCJFR5cGUAKAAAAFNldHRpbmdzJEludGVncmF0aW9uUHJvamVjdFNldHRpbmdzUGFydAAIT2Jzb2xldGVFbmFibGVVcmxFbmNvZGluZwAAAAMzALwCAAAFJElEABAAAAAA2ATNzy8zsVyMZNPxhKR3MQIkVHlwZQAfAAAAU2V0dGluZ3MkQ29uZmlndXJhdGlvblNldHRpbmdzAARDb25maWd1cmF0aW9ucwBjAgAAEDAAAwAAAAMxAFQCAAAFJElEABAAAAAAP4n8tkGqtkGlB01zarAlxgIkVHlwZQAdAAAAU2V0dGluZ3MkU2VydmVyQ29uZmlndXJhdGlvbgACQXBwbGljYXRpb25Sb290VXJsABcAAABodHRwOi8vbG9jYWxob3N0OjgwODAvAARDb25zdGFudFZhbHVlcwAMAAAAEDAAAwAAAAAEQ3VzdG9tU2V0dGluZ3MAjAAAABAwAAMAAAADMQB9AAAABSRJRAAQAAAAAHi+gsSIbFREuQciJ/ZAGw8CJFR5cGUAFwAAAFNldHRpbmdzJEN1c3RvbVNldHRpbmcAAk5hbWUAIAAAAGNvbS5tZW5kaXguY29yZS5TYW1lU2l0ZUNvb2tpZXMAAlZhbHVlAAcAAABTdHJpY3QAAAACRGF0YWJhc2VOYW1lAAkAAABkZWZhdWx0MwACRGF0YWJhc2VQYXNzd29yZAABAAAAAAJEYXRhYmFzZVR5cGUABwAAAEhzcWxkYgACRGF0YWJhc2VVcmwAAQAAAAAIRGF0YWJhc2VVc2VJbnRlZ3JhdGVkU2VjdXJpdHkAAAJEYXRhYmFzZVVzZXJOYW1lAAEAAAAAAkV4dHJhSnZtUGFyYW1ldGVycwABAAAAABBIdHRwUG9ydE51bWJlcgCQHwAAEE1heEphdmFIZWFwU2l6ZQAAAAAAAk5hbWUACAAAAERlZmF1bHQACE9wZW5BZG1pblBvcnQAAQhPcGVuSHR0cFBvcnQAARBTZXJ2ZXJQb3J0TnVtYmVyAJofAAAKVHJhY2luZwAAAAADNAAPAgAABSRJRAAQAAAAAGWBmiNWUjxTsFUwDkCaUAECJFR5cGUAFwAAAFNldHRpbmdzJE1vZGVsU2V0dGluZ3MAAkFmdGVyU3RhcnR1cE1pY3JvZmxvdwABAAAAAAhBbGxvd1VzZXJNdWx0aXBsZVNlc3Npb25zAAAQQmNyeXB0Q29zdAAKAAAAAkJlZm9yZVNodXRkb3duTWljcm9mbG93AAEAAAAAAkRlZmF1bHRUaW1lWm9uZUNvZGUAAQAAAAAIRW5hYmxlRGF0YVN0b3JhZ2VPcHRpbWlzdGljTG9ja2luZwAAAkZpcnN0RGF5T2ZXZWVrAAgAAABEZWZhdWx0AAJIYXNoQWxnb3JpdGhtAAgAAABTU0hBMjU2AAJIZWFsdGhDaGVja01pY3JvZmxvdwABAAAAAAJKYXZhVmVyc2lvbgAHAAAASmF2YTIxAAJSb3VuZGluZ01vZGUABwAAAEhhbGZVcAACU2NoZWR1bGVkRXZlbnRUaW1lWm9uZUNvZGUACAAAAEV0Yy9VVEMAAlNzbENlcnRpZmljYXRlQWxnb3JpdGhtAAgAAABTdW5YNTA5AAhVc2VEYXRhYmFzZUZvcmVpZ25LZXlDb25zdHJhaW50cwAACFVzZU9RTFZlcnNpb24yAAAIVXNlU3lzdGVtQ29udGV4dEZvckJhY2tncm91bmRUYXNrcwAAAAM1AN8AAAAFJElEABAAAAAAdw4jGD515Fufmj6CGCV5KQIkVHlwZQAcAAAAU2V0dGluZ3MkQ29udmVudGlvblNldHRpbmdzAARBY3Rpb25BY3Rpdml0eURlZmF1bHRDb2xvcnMADAAAABAwAAMAAAAAAkRlZmF1bHRBc3NvY2lhdGlvblN0b3JhZ2UABgAAAFRhYmxlAAJEZWZhdWx0U2VxdWVuY2VGbG93TGluZVR5cGUADAAAAEJlemllckN1cnZlAAhMb3dlckNhc2VNaWNyb2Zsb3dWYXJpYWJsZXMAAAADNgDMAQAABSRJRAAQAAAAAMOzKqDTeYtbrEk6j4poilACJFR5cGUAGgAAAFNldHRpbmdzJExhbmd1YWdlU2V0dGluZ3MAAkRlZmF1bHRMYW5ndWFnZUNvZGUABgAAAGVuX1VTAARMYW5ndWFnZXMAXgEAABAwAAMAAAADMQCmAAAABSRJRAAQAAAAABti7aKgipJAqTE+TzBjLsICJFR5cGUADwAAAFRleHRzJExhbmd1YWdlAAhDaGVja0NvbXBsZXRlbmVzcwAAAkNvZGUABgAAAGVuX1VTAAJDdXN0b21EYXRlRm9ybWF0AAEAAAAAAkN1c3RvbURhdGVUaW1lRm9ybWF0AAEAAAAAAkN1c3RvbVRpbWVGb3JtYXQAAQAAAAAAAzIApgAAAAUkSUQAEAAAAAAtO1KfC3Z9Taj3rm5wB/7aAiRUeXBlAA8AAABUZXh0cyRMYW5ndWFnZQAIQ2hlY2tDb21wbGV0ZW5lc3MAAQJDb2RlAAYAAABubF9OTAACQ3VzdG9tRGF0ZUZvcm1hdAABAAAAAAJDdXN0b21EYXRlVGltZUZvcm1hdAABAAAAAAJDdXN0b21UaW1lRm9ybWF0AAEAAAAAAAAAAzcAYQAAAAUkSUQAEAAAAACEUrRNWZ4FVZVUbGnMf2GOAiRUeXBlAB0AAABTZXR0aW5ncyRDZXJ0aWZpY2F0ZVNldHRpbmdzAARDZXJ0aWZpY2F0ZXMADAAAABAwAAMAAAAAAAM4APwAAAAFJElEABAAAAAAzwKefvwCFFOSRHnMeul3ZwIkVHlwZQAmAAAAU2V0dGluZ3MkV29ya2Zsb3dzUHJvamVjdFNldHRpbmdzUGFydAAQRGVmYXVsdFRhc2tQYXJhbGxlbGlzbQADAAAABE9uV29ya2Zsb3dFdmVudAAMAAAAEDAAAgAAAAACVXNlckVudGl0eQAMAAAAU3lzdGVtLlVzZXIAClVzZXJ0YXNrT25TdGF0ZUNoYW5nZUV2ZW50ABBXb3JrZmxvd0VuZ2luZVBhcmFsbGVsaXNtAAUAAAAKV29ya2Zsb3dPblN0YXRlQ2hhbmdlRXZlbnQAAAM5AGEAAAAFJElEABAAAAAAgm59e4wqbFqIBd0OffUBPgIkVHlwZQAfAAAAU2V0dGluZ3MkSmFyRGVwbG95bWVudFNldHRpbmdzAARFeGNsdXNpb25zAAwAAAAQMAACAAAAAAADMTAAfQAAAAUkSUQAEAAAAAC1ponqPFcHV6P8FzUnPEgzAiRUeXBlAB4AAABTZXR0aW5ncyREaXN0cmlidXRpb25TZXR0aW5ncwACQmFzZWRPblZlcnNpb24AAQAAAAAISXNEaXN0cmlidXRhYmxlAAACVmVyc2lvbgABAAAAAAAAAA==" + }, + { + "unit_id": "57d2ecb5-6c2f-4ffa-a0b3-a37a40da3adf", + "container_id": "80748544-417e-434a-a27a-354ba38115de", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Texts$SystemTextCollection", + "contents": "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YW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ADAAAABEZSBvcGdlZ2V2ZW4gd2FhcmRlIG1hZyBhbGxlZW4gY2lqZmVycyBiZXZhdHRlbgAAAAAAAzUA5QEAAAUkSUQAEAAAAACdGcoU7afbQIWCPv52la0PAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAgAAAAbWVuZGl4LnBsYXRmb3JtLmludmFsaWRfaW50ZWdlcgADVGV4dABzAQAABSRJRAAQAAAAAM1vpacrxT1Iqus8qX4qmV8CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zADcBAAAQMAADAAAAAzEAjgAAAAUkSUQAEAAAAADMSgoHlvnMU7xMbI2/hyq4AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAwAAAAVGhpcyB2YWx1ZSBzaG91bGQgYmUgYSBudW1iZXIgd2l0aG91dCBmcmFjdGlvbnMAAAMyAJcAAAAFJElEABAAAAAAz5sH6HBTn1KlkUlOGRPmIgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAOQAAAERlIG9wZ2VnZXZlbiB3YWFyZGUgbW9ldCBlZW4gZ2V0YWwgem9uZGVyIGRlY2ltYWxlbiB6aWpuAAAAAAADNgDgAQAABSRJRAAQAAAAAMH+oOVbTU5NkrWW8kiqV9MCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACIAAABtZW5kaXgucGxhdGZvcm0uaW50ZWdlcl9hYm92ZV9tYXgAA1RleHQAbAEAAAUkSUQAEAAAAACue9rbv+UgSoG1FNNG6AlAAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAwAQAAEDAAAwAAAAMxAIsAAAAFJElEABAAAAAAYZqsuqOTJ12hLsPA8/p3cgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALQAAAFRoaXMgdmFsdWUgc2hvdWxkIGJlIHNtYWxsZXIgdGhhbiAyMTQ3NDgzNjQ4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WbnNI/byRLS/AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dABDAAAAUGFzc3dvcmQgc2hvdWxkIGNvbnRhaW4gYXQgbGVhc3Qgb25lIG9mIHRoZSBmb2xsb3dpbmcgc3ltYm9sczogezF9AAADMgCmAAAABSRJRAAQAAAAAAAn0vQupfJSrL5jvUG3b7UCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AEgAAABIZXQgd2FjaHR3b29yZCBtb2V0IG1pbmltYWFsIGVlbiB2YW4gZGUgdm9sZ2VuZGUgc3ltYm9sZW4gYmV2YXR0ZW46IHsxfQAAAAAAAzI5ANsBAAAFJElEABAAAAAA47QAngD+fFuIvVF1GaYK3gIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAIQAAAG1lbmRpeC5wbGF0Zm9ybS5taXNzaW5nVXBwZXJjYXNlAANUZXh0AGgBAAAFJElEABAAAAAAv5LQZW3y3VyOQukucxMnRwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMALAEAABAwAAMAAAADMQCOAAAABSRJRAAQAAAAABYHXdllc8hZoBgi/Z+9EPICJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADAAAABQYXNzd29yZCBzaG91bGQgY29udGFpbiBhbiB1cHBlcmNhc2UgY2hhcmFjdGVyLgAAAzIAjAAAAAUkSUQAEAAAAACc+kcKdR+OV62Mg6XlzWBfAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAuAAAASGV0IHdhY2h0d29vcmQgbW9ldCBlZW4gaG9vZmRsZXR0ZXIgYmV2YXR0ZW4uAAAAAAADMzAA3AEAAAUkSUQAEAAAAABn8UoIllUcWID9S1EaSwXPAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAhAAAAbWVuZGl4LnBsYXRmb3JtLm1pc3NpbmdMb3dlcmNh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YXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAFwAAAHsxfSBpdGVtcyBnZXNlbGVjdGVlcmQAAAAAAAM2NAAzAgAABSRJRAAQAAAAACekFWiBRKZTmQ3XAYXuDIECJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ABcAAABteHVpLnN5cy5VSS5zeW5jX2Vycm9yAANUZXh0AMoBAAAFJElEABAAAAAAbky91LSbwFmmdLGRFwAIbQIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAjgEAABAwAAMAAAADMQCzAAAABSRJRAAQAAAAABtanoZ5NgZQoshMZcmckFACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AFUAAABZb3VyIGNoYW5nZXMgY2Fubm90IGJlIHN5bmNocm9uaXplZCBhdCB0aGlzIG1vbWVudC4gVGhleSB3aWxsIGJlIHN5bmNocm9uaXplZCBsYXRlci4AAAMyAMkAAAAFJElEABAAAAAAGoeY7k0AT1m5Gij3xOdOmwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAawAAAFV3IHdpanppZ2luZ2VuIGt1bm5lbiBuaWV0IHdvcmRlbiBnZXN5bmNocm9uaXNlZXJkIG9wIGRpdCBtb21lbnQuIFplIHp1bGxlbiBsYXRlciB3b3JkZW4gZ2VzeW5jaHJvbmlzZWVyZC4AAAAAAAM2NQCCAQAABSRJRAAQAAAAAMrcOYvD8RxejCI0OI89BzICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ABUAAABtZW5kaXguc2lnbmluLmNhbmNlbAADVGV4dAAbAQAABSRJRAAQAAAAADiNoIZFVVRQlF0J2j3EpvACJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAN8AAAAQMAADAAAAAzEAaAAAAAUkSUQAEAAAAABwO2Z0Ioo8XZjRPAQP1AZeAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4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c3RldW5kLiBQcm9iZWVyIGhldCBhbHN0dWJsaWVmdCBub2dtYWFscyBuYWRhdCBkZSBodWlkaWdlIHN5bmNocm9uaXNhdGllIGlzIHZvbHRvb2lkLgAAAzIA2gAAAAUkSUQAEAAAAACmk7zkak7IXbVDpqfC3zriAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAB8AAAAUGVyZm9ybWluZyBzaW11bHRhbmVvdXMgc3luY2hyb25pemF0aW9ucyBpcyBub3Qgc3VwcG9ydGVkLiBQbGVhc2UgdHJ5IGFnYWluIGFmdGVyIHRoZSBjdXJyZW50IHN5bmNocm9uaXphdGlvbiBpcyBjb21wbGV0ZWQuAAAAAAADOTEAkQEAAAUkSUQAEAAAAAAe0D3j8AbkW5LbSC3r6cJyAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAhAAAAbXh1aS53aWRnZXQuRGF0YUdyaWQucGFnZV9udW1iZXIAA1RleHQAHgEAAAUkSUQAEAAAAAA+mL9/g/1PX4+kimz7DC9xAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDiAAAAEDAAAwAAAAMxAGkAAAAFJElEABAAAAAAY6rIF485a1CHstLdDsGRtQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQACwAAAFBhZ2luYSB7MX0AAAMyAGcAAAAFJElEABAAAAAA+RSIwZ2uvFaB7j+cB1o5agIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQACQAAAFBhZ2UgezF9AAAAAAADOTIAngEAAAUkSUQAEAAAAADiK+KUO+ZzXbrMyBxMw7IrAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQApAAAAbXh1aS53aWRnZXQubmF0aXZlLk5hdmlnYXRpb24ucmVzdGFydEFwcAADVGV4dAAjAQAABSRJRAAQAAAAADJr/GmM5SZWhdaaGp7x04MCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAOcAAAAQMAADAAAAAzEAawAAAAUkSUQAEAAAAABro3YOypFVVp56l4SoQZE6AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAANAAAASGVyc3RhcnQgYXBwAAADMgBqAAAABSRJRAAQAAAAAN7jcRn8PRZRgvz46xhzXq4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAwAAABSZXN0YXJ0IGFwcAAAAAAAAzkzAJcBAAAFJElEABAAAAAAKUj5mOYhl1iZQ72hlwa7XAIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJwAAAG14dWkud2lkZ2V0Lm5hdGl2ZS5OYXZpZ2F0aW9uLmNsb3NlQXBwAANUZXh0AB4BAAAFJElEABAAAAAAzQg5A0XXqV6g1xlLdIRJVwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA4gAAABAwAAMAAAADMQBoAAAABSRJRAAQAAAAADIBSZGJEX5dovxT54Fujj4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AAoAAABTbHVpdCBhcHAAAAMyAGgAAAAFJElEABAAAAAAhV3ZGJPLelaSWAmSmdMoHwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQACgAAAENsb3NlIGFwcAAAAAAAAzk0ACQCAAAFJElEABAAAAAA2Wz3O8xGhVW5ZR/il7M2qgIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkASwAAAG1lbmRpeC5wbGF0Zm9ybS53b3JrZmxvd3MuY2FudF9vcGVuX3dvcmtmbG93X2FkbWluX3BhZ2Vfd29ya2Zsb3dfbm90X2ZvdW5kAANUZXh0AIcBAAAFJElEABAAAAAA5EgowOqzbFyzzO09zvWU9QIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMASwEAABAwAAMAAAADMQCeAAAABSRJRAAQAAAAAO9vIaNnn11frOSrTggzQI4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AEAAAABLYW4gZGUgQWRtaW4gcGFnaW5hIG5pZXQgb3BlbmVuLCB3YW50IGRlIHdvcmtmbG93IGJlc3RhYXQgbmlldC4AAAMyAJsAAAAFJElEABAAAAAA2+BGH6CEy1epd/CW5LJxbwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAPQAAAENhbuKAmXQgb3BlbiB0aGUgd29ya2Zsb3cgcGFnZSwgdGhlIFdvcmtmbG93IGRvZXMgbm90IGV4aXN0LgAAAAAAAzk1AKgCAAAFJElEABAAAAAAtAxKPYooSVGyKPqy/hwoVwIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkASgAAAG1lbmRpeC5wbGF0Zm9ybS53b3JrZmxvd3MuY2FudF9vcGVuX3dvcmtmbG93X2FkbWluX3BhZ2VfcGFnZV9ub3RfcHJvdmlkZWQAA1RleHQADAIAAAUkSUQAEAAAAACp0iUWgDleWaASZc8O2RdfAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDQAQAAEDAAAwAAAAMxAO4AAAAFJElEABAAAAAA1T2usBu5t16AmP++XlnxkwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAkAAAAEthbiBkZSBBZG1pbiBwYWdpbmEgbmlldCBvcGVuZW4sIHdhbnQgZGUgd29ya2Zsb3cgcGFnZSBpcyBuaWV0IGdlZGVmaW5pZWVyZC4gTmVlbSBjb250YWN0IG9wIG1ldCBkZSBhcHBsaWNhdGllYmVoZWVyZGVyIG9tIGRlemUgZm91dCB0ZSBtZWxkZW4uAAADMgDQAAAABSRJRAAQAAAAAL/71nieOR5anUXqZy0X2CgCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AHIAAABDYW7igJl0IG9wZW4gdGhlIHdvcmtmbG93IHBhZ2UsIHRoZSBXb3JrZmxvdyBBZG1pbiBwYWdlIGlzIG5vdCBkZWZpbmVkLiBQbGVhc2UgY29udGFjdCB5b3VyIFN5c3RlbSBBZG1pbmlzdHJhdG9yLgAAAAAAAzk2AKMCAAAFJElEABAAAAAAI/X0BOVgLFm0CJPwq09rqQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkATQAAAG1lbmRpeC5wbGF0Zm9ybS53b3JrZmxvd3MuY2FudF9vcGVuX3dvcmtmbG93X2FkbWluX3BhZ2VfZGVmaW5pdGlvbl9ub3RfZm91bmQAA1RleHQABAIAAAUkSUQAEAAAAABt4mX1FlPzXKEMbf/bwUfFAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDIAQAAEDAAAwAAAAMxAOwAAAAFJElEABAAAAAAXFznAdAp3V6fF0rrN+eJMQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAjgAAAEthbiBkZSBBZG1pbiBwYWdpbmEgbmlldCBvcGVuZW4sIHdhbnQgZGUgd29ya2Zsb3cgZGVmaW5pdGllIGlzIHZlcndpamRlcmQuIE5lZW0gY29udGFjdCBvcCBtZXQgZGUgYXBwbGljYXRpZWJlaGVlcmRlciBvbSBkZXplIGZvdXQgdGUgbWVsZGVuLgAAAzIAygAAAAUkSUQAEAAAAABzVsKsZPjRVrgO0Gul7WJdAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dABsAAAAQ2Fu4oCZdCBzaG93IHRoZSBBZG1pbiBwYWdlLCB0aGUgd29ya2Zsb3cgZGVmaW5pdGlvbiB3YXMgZGVsZXRlZC4gUGxlYXNlIGNvbnRhY3QgeW91ciBTeXN0ZW0gQWRtaW5pc3RyYXRvci4AAAAAAAM5NwAiAgAABSRJRAAQAAAAAOhJFkEYjhpcsgk/WkaoBNwCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AEIAAABtZW5kaXgucGxhdGZvcm0ud29ya2Zsb3dzLmNhbnRfb3Blbl90YXNrX2Fzc2lnbmVkX3RvX3NvbWVvbmVf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IwAAAERlbGV0aW5nIHlvdXJzZWxmIGlzIG5vdCBwb3NzaWJsZS4AAAMyAIMAAAAFJElEABAAAAAABmBZ90zc6FCMOO/dAGXUUgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAJQAAAEplemVsZiB2ZXJ3aWpkZXJlbiBpcyBuaWV0IG1vZ2VsaWprLgAAAAAAAzEwMQD9AQAABSRJRAAQAAAAAIIOKs7tI7pfjT+qUj9EK3QCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ADsAAABtZW5kaXgucGxhdGZvcm0uZGVsZXRpbmdfb2JqZWN0X2ZhaWxlZF9iZWNhdXNlX29mX3NlY3VyaXR5AANUZXh0AHABAAAFJElEABAAAAAAh7AuluduPFGd8Lx3h6TlWwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMANAEAABAwAAMAAAADMQCLAAAABSRJRAAQAAAAAFJf3zXdMZJbj+3RHua+doACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AC0AAABEZWxldGluZyBvYmplY3QgZmFpbGVkIGZvciBzZWN1cml0eSByZWFzb25zLgAAAzIAlwAAAAUkSUQAEAAAAADkFsaLNs1MUKZkvD+LSzL9AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAA5AAAARWVuIG9iamVjdCB2ZXJ3aWpkZXJlbiBpcyBnZWZhYWxkIG9tIHZlaWxpZ2hlaWRzcmVkZW5lbi4AAAAAAAMxMDIA2AEAAAUkSUQAEAAAAACHJpJN1HjQVLFqfYt3u5ZaAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQApAAAAbWVuZGl4LnBsYXRmb3JtLnVuc3VwcG9ydGVkX2ltYWdlX2Zvcm1hdAADVGV4dABdAQAABSRJRAAQAAAAABHLKZWiUAJeoK8RjaQxFYACJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zACEBAAAQMAADAAAAAzEAhAAAAAUkSUQAEAAAAADSRvZot12YX64QkrCHTUYCAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAmAAAASW1hZ2UgdHlwZSBvZiAnezF9JyBpcyBub3Qgc3VwcG9ydGVkLgAAAzIAiwAAAAUkSUQAEAAAAADCVCs9hWXMW7/rn2MCM8txAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAtAAAAQWZiZWVsZGluZ3R5cGUgJ3sxfScgd29yZHQgbmlldCBvbmRlcnN0ZXVuZC4AAAAAAAMxMDMAPwIAAAUkSUQAEAAAAABfhJUl3FrjXbiVO+ho2qpaAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQBBAAAAbWVuZGl4LnBsYXRmb3JtLmNvbmN1cnJlbnRfZXhlY3V0aW9uX25vdF9hbGxvd2VkX2Zvcl90aGlzX2FjdGlvbgADVGV4dACsAQAABSRJRAAQAAAAAIaP8UJI29JWvD/k0E5O6ogCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAHABAAAQMAADAAAAAzEArQAAAAUkSUQAEAAAAAASheBR2yO1VZg52A1V285yAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dABPAAAAQWN0aW9uICd7MX0nIGhhcyBub3QgYmVlbiBleGVjdXRlZCwgYXMgaXQgZG9lcyBub3QgYWxsb3cgY29uY3VycmVudCBleGVjdXRpb24uAAADMgCxAAAABSRJRAAQAAAAAJC4gu84nUVbgRUQX3ogEFQCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AFMAAABBY3RpZSAnezF9JyBpcyBuaWV0IHVpdGdldm9lcmQsIG9tZGF0IGdlbGlqa3RpamRpZ2UgdWl0dm9lcmluZyBuaWV0IGlzIHRvZWdlc3RhYW4uAAAAAAADMTA0ALEBAAAFJElEABAAAAAAdc6tkNTA2l2C41XTIcbzVAIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAIwAAAG1lbmRpeC5saWIuVmFsaWRhdGlvbnMuZmllbGRfaXNzdWUAA1RleHQAPAEAAAUkSUQAEAAAAACXfDhOLuw/XojO9IJYPB24AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAAAQAAEDAAAwAAAAMxAHQAAAAFJElEABAAAAAAHvV1burn3VmkuJA5oNQa1gIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAFgAAAHsxfSBoYXMgYW4gaXNzdWU6IHsyfQAAAzIAegAAAAUkSUQAEAAAAACxnz5EPpURV76rFvZvBcwXAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAcAAAAezF9IGhlZWZ0IGVlbiBwcm9ibGVlbTogezJ9AAAAAAADMTA1AJkBAAAFJElEABAAAAAAcjH0dw0eAlmtvsV5mVp0dQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAHQAAAG14dWkud2lkZ2V0Lkxpc3RWaWV3LnJlZnJlc2gAA1RleHQAKgEAAAUkSUQAEAAAAADptI0EWvrPVKXQRJfSxzHOAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDuAAAAEDAAAwAAAAMxAGwAAAAFJElEABAAAAAAz9tie0Lg+V+8PkFGcxE3IwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQADgAAAFJlZnJlc2gvQ2xlYXIAAAMyAHAAAAAFJElEABAAAAAA3wVKfuCLPVeiS64cLKnNUAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAEgAAAFZlcm5pZXV3ZW4vV2lzc2VuAAAAAAADMTA2AO8BAAAFJElEABAAAAAAkaNNRK8F1lWScMtzq0UPVgIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAPwAAAG1lbmRpeC5wbGF0Zm9ybS53b3JrZmxvd3MuY2FudF9sb2NrX3dvcmtmbG93X2RlZmluaXRpb25fbG9ja2VkAANUZXh0AF4BAAAFJElEABAAAAAA3/WwcX83X1iEfneWK+W40gIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAIgEAABAwAAMAAAADMQCJAAAABSRJRAAQAAAAAJDsx8sTFS9atPWKYVFX70gCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ACsAAABUaGUgd29ya2Zsb3cgZGVmaW5pdGlvbiBpcyBhbHJlYWR5IGxvY2tlZC4AAAMyAIcAAAAFJElEABAAAAAA+weLBmJsS1K4f06eimQ8MAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAKQAAAERlIHdvcmtmbG93IGRlZmluaXRpZSBpcyBhbCB2ZXJncmVuZGVsZC4AAAAAAAMxMDcA8wEAAAUkSUQAEAAAAABQdc9zs8mTUa0J5xYHjrRqAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQBFAAAAbWVuZGl4LnBsYXRmb3JtLndvcmtmbG93cy5jYW50X3VubG9ja193b3JrZmxvd19kZWZpbml0aW9uX25vdF9sb2NrZWQAA1RleHQAXAEAAAUkSUQAEAAAAABMkjRTNJgJWKef5Sf+u52WAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAgAQAAEDAAAwAAAAMxAIUAAAAFJElEABAAAAAALSEZ4q7lD1+2ZH08wdbo2wIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAJwAAAFRoZSB3b3JrZmxvdyBkZWZpbml0aW9uIGlzIG5vdCBsb2NrZWQuAAADMgCJAAAABSRJRAAQAAAAAHEp8Sb4SfZXhH3qomG6y2oCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ACsAAABEZSB3b3JrZmxvdyBkZWZpbml0aWUgaXMgbmlldCB2ZXJncmVuZGVsZC4AAAAAAAMxMDgAPwIAAAUkSUQAEAAAAAACQZWNnUpvVKDv4FtnhQJjAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQBGAAAAbWVuZGl4LnBsYXRmb3JtLndvcmtmbG93cy5jYW50X2luc3RhbnRpYXRlX3dvcmtmbG93X2RlZmluaXRpb25fbG9ja2VkAANUZXh0AKcBAAAFJElEABAAAAAAMzJ/PuEnxFqCbzyrUXiVgAIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAawEAABAwAAMAAAADMQCuAAAABSRJRAAQAAAAAICr96eVZ85fvacZB+OL2DcCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AFAAAABDYW4ndCBjcmVhdGUgYSBuZXcgd29ya2Zsb3cgaW5zdGFuY2UgYmVjYXVzZSB0aGUgd29ya2Zsb3cgZGVmaW5pdGlvbiBpcyBsb2NrZWQuAAADMgCrAAAABSRJRAAQAAAAACQ4491xXwRXnGZamqa3uK8CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AE0AAABLYW4gZ2VlbiBuaWV1d2Ugd29ya2Zsb3cgc3RhcnRlbiBvbWRhdCBkZSB3b3JrZmxvdyBkZWZpbml0aWUgdmVyZ3JlbmRlbGQgaXMuAAAAAAADMTA5AI4CAAAFJElEABAAAAAAcHsycj3Lm1+/8CB/ZWJS2AIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAUwAAAG1lbmRpeC5wbGF0Zm9ybS53b3JrZmxvd3MuY2FudF9pbnN0YW50aWF0ZV93b3JrZmxvd19zdWJfd29ya2Zsb3dfZGVmaW5pdGlvbl9sb2NrZWQAA1RleHQA6QEAAAUkSUQAEAAAAADdIPdGnffMWLb/xuVl5sIpAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwCtAQAAEDAAAwAAAAMxAMQAAAAFJElEABAAAAAA0NoZab60rFixCuPrDb9r8wIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAZgAAAFdvcmtmbG93IHsxfSBpcyBsb2NrZWQuIFlvdSBjYW4ndCBjYWxsIGl0IGluIHRoaXMgd29ya2Zsb3cuIFBsZWFzZSBjb250YWN0IHlvdXIgU3lzdGVtIEFkbWluaXN0cmF0b3IuAAADMgDXAAAABSRJRAAQAAAAAMzseaaqX3FShf33FoNSkn0CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AHkAAABXb3JrZmxvdyB7MX0gaXMgdmVyZ3JlbmRlbGQgZW4ga2FuIG5pZXQgd29yZGVuIGFhbmdlcm9lcGVuIGluIGRlemUgd29ya2Zsb3cuIE5lZW0gY29udGFjdCBvcCBtZXQgZGUgYXBwbGljYXRpZWJlaGVlcmRlci4AAAAAAAMxMTAABQIAAAUkSUQAEAAAAADRI246a2n+XJRVBoBM/6NBAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQA7AAAAbWVuZGl4LnBsYXRmb3JtLndvcmtmbG93cy5jYW50X25vdGlmeV93b3JrZmxvd193cm9uZ19zdGF0ZQADVGV4dAB4AQAABSRJRAAQAAAAACWkQ9DkaENVgUv9Qmu7WC8CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zADwBAAAQMAADAAAAAzEAkgAAAAUkSUQAEAAAAAA5Yvfnv0h4WaVKcmXEo7WmAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAA0AAAAQ2FuJ3Qgbm90aWZ5IHdvcmtmbG93IHsyfSBiZWNhdXNlIGl0IGhhcyBzdGF0ZSB7MX0uAAADMgCYAAAABSRJRAAQAAAAAJaArB4+GDpWhHjb3n+FToECJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ADoAAABXb3JrZmxvdyB7Mn0ga2FuIG5pZXQgd29yZGVuIGdlbm90aWZpY2VlcmQgaW4gc3RhdHVzIHsxfS4AAAAAAAMxMTEAcAIAAAUkSUQAEAAAAABAlrVIU+FQV4St50v3rLKxAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAzAAAAbWVuZGl4LnBsYXRmb3JtLndvcmtmbG93cy5hZGhvY19qdW1wdG9fbm90X2FsbG93ZWQAA1RleHQA6wEAAAUkSUQAEAAAAAD4Nf82HnM1VIBGeAKzQRbFAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwCvAQAAEDAAAwAAAAMxAMsAAAAFJElEABAAAAAA8AqWU7RpOVWax4g15TuMmAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAbQAAAENhbid0IGdlbmVyYXRlIGp1bXAtdG8gb3B0aW9ucyBmb3IgdGhpcyB3b3JrZmxvdyBpbnN0YW5jZSBiZWNhdXNlIGl0cyBjdXJyZW50IHN0YXRlICh7MX0pIGRvZXMgbm90IGFsbG93IGl0LgAAAzIA0gAAAAUkSUQAEAAAAAAPjpevotvrUbWm3B5/0CqLAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAB0AAAAS2FuIGRlIGp1bXAtdG8gb3B0aWVzIG5pZXQgZ2VuZXJlcmVuIHZvb3IgZGV6ZSB3b3JrZmxvdyBpbnN0YW50aWUsIG9tZGF0IGRlIGh1aWRpZ2Ugc3RhdHVzICh7MX0pIGhldCBuaWV0IHRvZXN0YWF0LgAAAAAAAzExMgD/AQAABSRJRAAQAAAAAOyNQWe1MaBUgFbUk6TDEkYCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AEIAAABtZW5kaXgucGxhdGZvcm0ud29ya2Zsb3dzLm9ubHlfbmFtZWRfdXNlcnNfY2FuX2NvbXBsZXRlX3VzZXJfdGFzawADVGV4dABrAQAABSRJRAAQAAAAAJYwuSH6EPtcrY43gT4OtJICJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAC8BAAAQMAADAAAAAzEAiQAAAAUkSUQAEAAAAACW/fTDtllWUJG53HHKOTowAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAArAAAAT25seSBuYW1lZCB1c2VycyBjYW4gY29tcGxldGUgYSB1c2VyIHRhc2suAAADMgCUAAAABSRJRAAQAAAAAP5vrmCkvA9ZpcMZhJp+WRACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ADYAAABBbGxlZW4gaW5nZWxvZ2RlIGdlYnJ1aWtlcnMga3VubmVuIGVlbiB0YWFrIGFmcm9uZGVuLgAAAAAAAzExMwBUAgAABSRJRAAQAAAAANmaJaUDeWBQhqS1N72fefICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AEgAAABtZW5kaXgucGxhdGZvcm0ud29ya2Zsb3dzLmNhbnRfY29tcGxldGVfdGFza19vdXRjb21lX2FscmVhZHlfc2V0X2J5X3lvdQADVGV4dAC6AQAABSRJRAAQAAAAAB+MefDaX+5ehra71xKbXGICJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAH4BAAAQMAADAAAAAzEAtwAAAAUkSUQAEAAAAABpiqioYfYTWYcERxUzcsAGAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dABZAAAAWW91IGNhbid0IGNvbXBsZXRlIHRoaXMgdXNlciB0YXNrIGJlY2F1c2UgYW4gb3V0Y29tZSBoYXMgYWxyZWFkeSBiZWVuIHNldCBmb3IgdGhpcyB1c2VyLgAAAzIAtQAAAAUkSUQAEAAAAABj1tDGOD0rVo3DOeYSA8JiAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABXAAAASmUga3VudCBkZXplIHRhYWsgbmlldCBhZnJvbmRlbiwgd2FudCBlciBpcyBhbCBlZW4gdWl0a29tc3QgZ2V6ZXQgdm9vciBkZXplIGdlYnJ1aWtlci4AAAAAAAMxMTQAGQIAAAUkSUQAEAAAAAAK93zBgQVLU5WkY/yBkRkPAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQA/AAAAbWVuZGl4LnBsYXRmb3JtLndvcmtmbG93cy5jYW50X2NvbXBsZXRlX3Rhc2tfYWxyZWFkeV9jb21wbGV0ZWQAA1RleHQAiAEAAAUkSUQAEAAAAAAFCUQkBvFmXbBel5L0tDc6AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwBMAQAAEDAAAwAAAAMxAKEAAAAFJElEABAAAAAAncX5Ay2BxViYuJjfz+0vyQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAQwAAAFlvdSBjYW4ndCBjb21wbGV0ZSB0aGlzIHVzZXIgdGFzayBiZWNhdXNlIGl0IGlzIGFscmVhZHkgY29tcGxldGVkLgAAAzIAmQAAAAUkSUQAEAAAAADCi/pq8QzUVZTbArBTP0xVAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAA7AAAASmUga3VudCBkZXplIHRhYWsgbmlldCBhZnJvbmRlbiwgd2FudCBkZXplIGlzIGFsIGFmZ2Vyb25kLgAAAAAAAzExNQAVAgAABSRJRAAQAAAAAGz06cZgl4FRl+EIkoxta74CJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AD0AAABtZW5kaXgucGxhdGZvcm0ud29ya2Zsb3dzLmNhbnRfY29tcGxldGVfdGFza19ub3RfaW5fcHJvZ3Jlc3MAA1RleHQAhgEAAAUkSUQAEAAAAABw7P7qdkgeXa0UlCQmu6jtAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwBKAQAAEDAAAwAAAAMxAJ8AAAAFJElEABAAAAAAfsDkS7IK6lyyQPUJOcZPowIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAQQAAAFlvdSBjYW4ndCBjb21wbGV0ZSB0aGlzIHVzZXIgdGFzayBiZWNhdXNlIGl0IGlzIG5vdCBpbiBwcm9ncmVzcy4AAAMyAJkAAAAFJElEABAAAAAAP6z/HhZou1KCxtccXk1xKgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAOwAAAEplIGt1bnQgZGV6ZSB0YWFrIG5pZXQgYWZyb25kZW4sIHdhbnQgZGV6ZSBpcyBuaWV0IGFjdGllZi4AAAAAAAMxMTYAYQIAAAUkSUQAEAAAAAABcsXsYRSSV4soK8JG2qYfAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQBGAAAAbWVuZGl4LnBsYXRmb3JtLndvcmtmbG93cy5jYW50X2NvbXBsZXRlX3Rhc2tfd29ya2Zsb3dfbm90X2luX3Byb2dyZXNzAANUZXh0AMkBAAAFJElEABAAAAAAxylK1paTtFGG3OUMUbV22AIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAjQEAABAwAAMAAAADMQDAAAAABSRJRAAQAAAAAL3EuIg9nmJbmsC2uj/02EQCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AGIAAABZb3UgY2FuJ3QgY29tcGxldGUgdGhpcyB1c2VyIHRhc2sgYmVjYXVzZSB0aGUgd29ya2Zsb3cgaW5zdGFuY2UgY29udGFpbmluZyBpdCBpcyBub3QgaW4gcHJvZ3Jlc3MuAAADMgC7AAAABSRJRAAQAAAAAL7j5w0egk9XhwNDi2ddmlMCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AF0AAABKZSBrdW50IGRlemUgdGFhayBuaWV0IGFmcm9uZGVuLCB3YW50IGRlIHdvcmtmbG93IHdhYXIgZGV6ZSBvbmRlcmRlZWwgdmFuIGlzIGlzIG5pZXQgYWN0aWVmLgAAAAAAAzExNwCHAgAABSRJRAAQAAAAAPjgJFrdAiRWkjCKickfWM4CJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ADwAAABtZW5kaXgucGxhdGZvcm0ud29ya2Zsb3dzLmNhbnRfY29tcGxldGVfdGFza19pbnRlcm5hbF9lcnJvcgADVGV4dAD5AQAABSRJRAAQAAAAAK+b+FmRK3FambGRb3dS0sMCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAL0BAAAQMAADAAAAAzEA2AAAAAUkSUQAEAAAAACI2InQTxKKUJL9xy2JQk1lAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAB6AAAAWW91IGNhbuKAmXQgY29tcGxldGUgdGhpcyB1c2VyIHRhc2sgYmVjYXVzZSB0aGVyZSBpcyBhbiBpbnRlcm5hbCBpc3N1ZSB3aXRoIGl0LiBQbGVhc2UgY29udGFjdCB5b3VyIFN5c3RlbSBBZG1pbmlzdHJhdG9yLgAAAzIA0wAAAAUkSUQAEAAAAAD4O6SaSOvrUoIYTH8fdHGDAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAB1AAAASmUga3VudCBkZXplIHRhYWsgbmlldCBhZnJvbmRlbiwgd2FudCBlciBpcyBlZW4gaW50ZXJuZSBmb3V0IG9wZ2V0cmVkZW4uIE5lZW0gY29udGFjdCBvcCBtZXQgZGUgYXBwbGljYXRpZWJlaGVlcmRlci4AAAAAAAMxMTgADgIAAAUkSUQAEAAAAAA0y2r+BHGdX73eUEnQ3p/IAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAbAAAAbXh1aS5zeXMuVUkucGFnZV9ub3RfZm91bmQAA1RleHQAoQEAAAUkSUQAEAAAAABNZ1qLr4fJWpt3sBo5b7mfAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwBlAQAAEDAAAwAAAAMxAKkAAAAFJElEABAAAAAARnO9gBbZEV6C8BQ2hoGgLQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQASwAAAFRoZSBwYWdlIHlvdSByZXF1ZXN0ZWQgd2FzIG5vdCBmb3VuZC4gWW91IGFyZSByZWRpcmVjdGVkIHRvIHRoZSBob21lIHBhZ2UuAAADMgCqAAAABSRJRAAQAAAAAIsxnpFC2bBUqql+5KrB1NUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AEwAAABEZXplIHBhZ2luYSBrb24gbmlldCBnZXZvbmRlbiB3b3JkZW4uIFUgd29yZCBkb29ydmVyd2V6ZW4gbmFhciBkZSBob21lIHBhZ2UAAAAAAAAA" + }, + { + "unit_id": "1fcc2823-c303-5a27-b6c7-78b04c96c88f", + "container_id": "80748544-417e-434a-a27a-354ba38115de", + "module": null, + "containment": "ProjectConversion", + "name": "", + "type": "Projects$ProjectConversion", + "contents": "mi8AAAUkSUQAEAAAAAAjKMwfA8MnWrbHeLBMlsiPAiRUeXBlABsAAABQcm9qZWN0cyRQcm9qZWN0Q29udmVyc2lvbgAET25lVGltZUNvbnZlcnNpb25zAEEvAAAQMAADAAAAAzEAeAAAAAUkSUQAEAAAAADWvAmoV5zlXaz5H+bSnOqeAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQApAAAARGVkdXBsaWNhdGVEZXBlbmRlbnRHdWlkT2JqZWN0Q29udmVyc2lvbgAAAzIAdgAAAAUkSUQAEAAAAAAn5AFjj4ZnV6MEyEGFVBNUAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAnAAAARGlzYWJsZVN0cmljdFBhZ2VVcmxDaGVja2luZ0NvbnZlcnNpb24AAAMzAHEAAAAFJElEABAAAAAAq/waYlhAW1qCkueci1AF6QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIgAAAEludHJvZHVjZU1vZHVsZVNldHRpbmdzQ29udmVyc2lvbgAAAzQAcQAAAAUkSUQAEAAAAABQUO1Zmx+6UpzDLYQzv0ddAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAiAAAATWljcm9mbG93QWN0aW9uSW5mb0ljb25Db252ZXJzaW9uAAADNQBvAAAABSRJRAAQAAAAAOp/E3jXV91Zi35yngPIMAQCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACAAAABKYXJEZXBsb3ltZW50U2V0dGluZ3NDb252ZXJzaW9uAAADNgBnAAAABSRJRAAQAAAAAHgJRiM6GEBXlPGshhwAqFkCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABgAAABNeEltYWdlRm9ybWF0Q29udmVyc2lvbgAAAzcAaAAAAAUkSUQAEAAAAAC15BuesQkfXYaDSMR4DVYhAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAZAAAAQ29udmVydEh0dHBDb25maWd1cmF0aW9uAAADOABwAAAABSRJRAAQAAAAAMR8Oc+Eg1BYmvPDbucgDVkCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACEAAABDb252ZXJ0WG1sU2NoZW1hVGFyZ2V0TmFtZXNwYWNlcwAAAzkAbAAAAAUkSUQAEAAAAAA8Kn0UWf1WVZTmEg1dVx9JAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAdAAAAQ29udmVydEF0dHJpYnV0ZUNhcGFiaWxpdGllcwAAAzEwAG8AAAAFJElEABAAAAAAfX0d/Jg+KFGL4kXoxNMwjwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIAAAAENvbnZlcnRDb25zdW1lZE9EYXRhU2VydmljZU5hbWUAAAMxMQB3AAAABSRJRAAQAAAAACZu+58d0lVUkUlf15/nstwCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACgAAABDb252ZXJ0Q29uc3VtZWRPRGF0YUF0dHJpYnV0ZVJlbW90ZVR5cGUAAAMxMgBjAAAABSRJRAAQAAAAANnUAx/sm75YlNV8BAIEwakCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABQAAABDb252ZXJ0T0RhdGFWZXJzaW9uAAADMTMAawAAAAUkSUQAEAAAAABSdUQyYclYXbJo3R4wt0QxAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAcAAAAQ29udmVydENhcGFiaWxpdGllc1RvU291cmNlAAADMTQAegAAAAUkSUQAEAAAAACV+BwChHpXXIuOfaOzyUD/AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQArAAAAQ29udmVydE9EYXRhQXR0cmlidXRlVXBkYXRhYmxlQ2FwYWJpbGl0aWVzAAADMTUAfAAAAAUkSUQAEAAAAAB/+wG3b5EmWoD9NW3YxX+tAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAtAAAAQ29udmVydE9EYXRhVXBkYXRhYmxlQXNzb2NpYXRpb25DYXBhYmlsaXRpZXMAAAMxNgBoAAAABSRJRAAQAAAAAJf4/QdbZ19Zm7ASF3H2q/gCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABkAAABDb252ZXJ0S2V5UGFydFJlbW90ZVR5cGUAAAMxNwB6AAAABSRJRAAQAAAAACbTKwB5+jNbiHWn+Wl8KVYCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACsAAABDb252ZXJ0UmVwcmVzZW50c1N0cmVhbUZvck9EYXRhTWFwcGVkVmFsdWUAAAMxOABpAAAABSRJRAAQAAAAAGwCQ7m/q/xathfJa/TV4OACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABoAAABDb252ZXJ0Q3JlYXRhYmxlRGVsZXRhYmxlAAADMTkAcAAAAAUkSUQAEAAAAAAceID1E7MnVYfGD2hG8WcjAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAhAAAAQ29udmVydFB1Ymxpc2hlZE9EYXRhUmVzb3VyY2VJRHMAAAMyMABkAAAABSRJRAAQAAAAAGQRanp3aHVfiLAoF77EVU0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABUAAABDb252ZXJ0TmF2aWdhYmlsaXR5MgAAAzIxAGAAAAAFJElEABAAAAAAu+O686jQkFidaQkKtFsRIQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAEQAAAENvbnZlcnRDb3VudGFibGUAAAMyMgB6AAAABSRJRAAQAAAAAEwDKY3Hm81akbjsW/SChOACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACsAAABDb252ZXJ0UHVibGlzaGVkT0RhdGFTZXJ2aWNlVmVyc2lvbkRlZmF1bHQAAAMyMwBsAAAABSRJRAAQAAAAAGR3cwU0Ms1eqO1hHBnUWkACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB0AAABDb252ZXJ0UHVibGlzaGVkT0RhdGFWZXJzaW9uAAADMjQAdgAAAAUkSUQAEAAAAABw8Wv+69KJWYrvIEhDyz86AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAnAAAAQ29udmVydFB1Ymxpc2hlZE9EYXRhUmVzb3VyY2VVcGRhdGFibGUAAAMyNQBpAAAABSRJRAAQAAAAAEssyZpBxKVbi2jjUJXGqJwCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABoAAABDb252ZXJ0RmlsdGVyYWJsZVNvcnRhYmxlAAADMjYAdgAAAAUkSUQAEAAAAAD11kpTYSmKWa7eI/SmoF1lAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAnAAAAQ29udmVydEFwcFNlcnZpY2VBY3Rpdml0eURlZmF1bHRDb2xvcnMAAAMyNwBxAAAABSRJRAAQAAAAAMwNivFdN6JcnJp88Jl9ysQCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACIAAABKYXZhQWN0aW9uUXVldWVTZXR0aW5nc0NvbnZlcnNpb24AAAMyOABkAAAABSRJRAAQAAAAAFLsDK+YU6hRl1ssBTzz94ACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABUAAABMb29wU291cmNlQ29udmVyc2lvbgAAAzI5AGwAAAAFJElEABAAAAAAzUwThBpve1qfGDcy34DEdgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHQAAAFF1ZXVlU3lzdGVtQ29udGV4dENvbnZlcnNpb24AAAMzMABwAAAABSRJRAAQAAAAAFjsOdbT4WVRhZQ/xz30Dx4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACEAAABNaWNyb2Zsb3dRdWV1ZVNldHRpbmdzQ29udmVyc2lvbgAAAzMxAH8AAAAFJElEABAAAAAAvgcrxWg+W1+Vk2TllRm8MgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAMAAAAEJhc2ljUXVldWVDb25maWdQYXJhbGxlbGlzbUV4cHJlc3Npb25Db252ZXJzaW9uAAADMzIAaQAAAAUkSUQAEAAAAABfmcM46ketWpvkvHtVtJ3ZAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAaAAAAU2NoZWR1bGVkRXZlbnRzQ29udmVyc2lvbgAAAzMzAH8AAAAFJElEABAAAAAA++GkSY2qGFWTnqcgMraf9QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAMAAAAEVycm9uZW91c0lucHV0SW5BY3Rpb25JdGVtRXhwcmVzc2lvbnNDb252ZXJzaW9uAAADMzQAbgAAAAUkSUQAEAAAAAD5DZdhapW3UaD78GIWDIRMAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAfAAAARml4UGFyYW1ldGVyTWFwcGluZ3NDb252ZXJzaW9uAAADMzUAaQAAAAUkSUQAEAAAAAD6liuIXoIjVqg5AAgxY/aoAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAaAAAAU3RhdGljUmVzb3VyY2VzQ29udmVyc2lvbgAAAzM2AGoAAAAFJElEABAAAAAA2wljZXhWvFCY9vlcvh0sBAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGwAAAFBhZ2VUZW1wbGF0ZVR5cGVDb252ZXJzaW9uAAADMzcAZwAAAAUkSUQAEAAAAAAC/RwmZmbXX5F+BRbpmMvVAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAYAAAAUGFnaW5nQmFyVHlwZUNvbnZlcnNpb24AAAMzOABuAAAABSRJRAAQAAAAAL1r3wPyh4ZXlQcPt9NLNyMCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB8AAABOdW1iZXJPZlBhZ2VzVG9DbG9zZUNvbnZlcnNpb24AAAMzOQBrAAAABSRJRAAQAAAAACusz/OvC59XvKZ2jNNqovYCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABCb3R0b21CYXJJdGVtUGFnZUNvbnZlcnNpb24AAAM0MABnAAAABSRJRAAQAAAAADnSOZNTDgxZhoveXAh+NWgCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABgAAABBdGxhc0NhdGVnb3J5Q29udmVyc2lvbgAAAzQxAHMAAAAFJElEABAAAAAAAw3SKO+fi1WUfSSyYD/eLQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJAAAAFByb2dyZXNzaXZlV2ViQXBwU2V0dGluZ3NDb252ZXJzaW9uAAADNDIAeAAAAAUkSUQAEAAAAADtWiQx7iaZXI1qmSzgNxcBAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQApAAAARGVkdXBsaWNhdGVEZXNpZ25Qcm9wZXJ0eVZhbHVlQ29udmVyc2lvbgAAAzQzAHAAAAAFJElEABAAAAAAj9HdzsCa31qMWmhwUwBuqQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIQAAAE5hdGl2ZU5hbm9mbG93SG9tZVBhZ2VDb252ZXJzaW9uAAADNDQAeAAAAAUkSUQAEAAAAAD6qr3j8oGVXKrIZKPsyMDzAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQApAAAAVXBkYXRlRmlsZURvY3VtZW50c0FjY2Vzc1J1bGVzQ29udmVyc2lvbgAAAzQ1AGgAAAAFJElEABAAAAAAFA9OpcOxjVCwKP/FgrVUmgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGQAAAFNob3dQYWdlQWN0aW9uQ29udmVyc2lvbgAAAzQ2AHAAAAAFJElEABAAAAAAwvUPsmP5qFi3+7xT+63YKAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIQAAAE11bHRpcGxlUGFnZVBhcmFtZXRlcnNDb252ZXJzaW9uAAADNDcAdQAAAAUkSUQAEAAAAAA6gCVovs4UU7BuwA0mMbWUAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAQ29udmVydEJFTXVsdGlwbGVQcm9kdWNlckFuZENvbnN1bWVycwAAAzQ4AH0AAAAFJElEABAAAAAAgsPHtr3mbF63cPCatWcpjwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALgAAAENvbnZlcnRCdXNpbmVzc0V2ZW50c0ltcGxlbWVudGF0aW9uVHJpbVByZWZpeAAAAzQ5AHgAAAAFJElEABAAAAAAi5XPLZEFnleEp+b8KhvF4QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKQAAAENvbnZlcnRCdXNpbmVzc0V2ZW50c0VudW1lcmF0aW9uU25hcHNob3QAAAM1MABuAAAABSRJRAAQAAAAANh+TEI8+Axdua2D7HygLq4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB8AAABEaXN0cmlidXRpb25TZXR0aW5nc0NvbnZlcnNpb24AAAM1MQBsAAAABSRJRAAQAAAAANS4H1rBFE5crrq74GBazgoCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB0AAABDb252ZXJ0UmVtb3RlRW50aXR5UmVtb3RlSWRzAAADNTIAaQAAAAUkSUQAEAAAAABGmf2C9mM+UpoL30I6DI6HAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAaAAAASmF2YVZlcnNpb25GaWxlQ29udmVyc2lvbgAAAzUzAHIAAAAFJElEABAAAAAAj2RYpg/Y0FqWDHOpltOUOwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIwAAAFNoYXJlZENvbmZpZ3VyYXRpb25WYWx1ZUNvbnZlcnNpb24AAAM1NABmAAAABSRJRAAQAAAAACS/QCbPfxBcsKNZMz9bsJYCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABcAAABUYWJsZU1hcHBpbmdDb252ZXJzaW9uAAADNTUAXQAAAAUkSUQAEAAAAAAqP+0sKLe2VYbt01vSl5ToAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAOAAAAQ1NWQ29udmVyc2lvbgAAAzU2AHoAAAAFJElEABAAAAAAXqNGo83RtFePqVUOX/eGBwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKwAAAE1lc3NhZ2VEZWZpbml0aW9uVXBkYXRlT3JpZ2luYWxOYW1lQW5kUGF0aAAAAzU3AGgAAAAFJElEABAAAAAA5pEMbVmN4lCassnaJM2DUwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGQAAAENvbnZlcnRFbnVtZXJhdGlvblNvdXJjZQAAAzU4AG0AAAAFJElEABAAAAAA6JagMJETf1iwHINRMmVfwgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHgAAAENvbnZlcnRSZW1vdGVFbnVtZXJhdGlvblZhbHVlAAADNTkAbQAAAAUkSUQAEAAAAABDECU42vz6UoQU8HX1UGbNAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAQ29udmVydENvbmZpZ3VyYXRpb25NaWNyb2Zsb3cAAAM2MABwAAAABSRJRAAQAAAAAIRH8Sl098pZg2HkiaYOelsCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACEAAABBZGREZWZhdWx0QWNjZXB0SGVhZGVyQ29udmVyc2lvbgAAAzYxAHUAAAAFJElEABAAAAAAksWoE8tCoF6KgdkHwlak6gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJgAAAEFkZERlZmF1bHRDb250ZW50VHlwZUhlYWRlckNvbnZlcnNpb24AAAM2MgB2AAAABSRJRAAQAAAAAFntas2nVlNRoPykhjxDl3ICJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACcAAABPbGRTdHJpbmdCb2R5VG9WYWx1ZVRlbXBsYXRlQ29udmVyc2lvbgAAAzYzAHMAAAAFJElEABAAAAAAm7jayF/gklWlyrhx4SAxSQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJAAAAEtlZXBFbmNvZGVQYXJhbWV0ZXJzSW5VcmxDb252ZXJzaW9uAAADNjQAZwAAAAUkSUQAEAAAAAD7X/5jToBzXKLmY0nZvQ9HAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAYAAAAQ29udmVydFRvcFNraXBTdXBwb3J0ZWQAAAM2NQBqAAAABSRJRAAQAAAAAHE8DqkYyphVinTUMaWVQy4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABsAAABDb252ZXJ0RW51bWVyYXRpb25Bc1N0cmluZwAAAzY2AGMAAAAFJElEABAAAAAARvYNdc7psVW7Qbd3taGRzgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFAAAAENvbnZlcnROZXdOYW1lc3BhY2UAAAM2NwBbAAAABSRJRAAQAAAAAEn5qRJtfdlfjNeHV77N87YCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAAwAAABDb252ZXJ0UGF0aAAAAzY4AHsAAAAFJElEABAAAAAA0d0cvuywAVOYkE2nHKUQ6QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALAAAAEphdmFTY3JpcHRBY3Rpb25OYW5vZmxvd1BhcmFtZXRlckNvbnZlcnNpb24AAAM2OQBrAAAABSRJRAAQAAAAANykCP5sMRlSnq+5aMDMpAUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABNaWNyb2Zsb3dTdGFibGVJZENvbnZlcnNpb24AAAM3MABkAAAABSRJRAAQAAAAADG84FFteo9WkCVbzfa6Y4cCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABUAAABDYXNlVmFsdWVzQ29udmVyc2lvbgAAAzcxAGIAAAAFJElEABAAAAAAPmv9z0zUv1is7YgbIoS5oQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAEwAAAEZsb3dMaW5lQ29udmVyc2lvbgAAAzcyAGgAAAAFJElEABAAAAAAXK9SM5W23Vy5Ie9egtSUogIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGQAAAEphdmFEZXBlbmRlbmN5Q29udmVyc2lvbgAAAzczAHwAAAAFJElEABAAAAAACmF1ya9xSle28+6iBkoOKgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALQAAAEN1c3RvbVdpZGdldENsZWFySGlkZGVuUHJvcGVydGllc0NvbnZlcnNpb24yAAADNzQAcwAAAAUkSUQAEAAAAAAa4DWAexyiXZ1MVrnOuuWXAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAATXVsdGlwbGVTbmlwcGV0UGFyYW1ldGVyc0NvbnZlcnNpb24AAAM3NQB9AAAABSRJRAAQAAAAAOJ15ynuaNhbuEdBmuxEWsUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAC4AAABNdWx0aXBsZVNuaXBwZXRQYXJhbWV0ZXJzQXBwZWFyYW5jZUNvbnZlcnNpb24AAAM3NgCAAAAABSRJRAAQAAAAANodVI6kXtpQvVAqNyU6I0sCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lADEAAABBdHRyaWJ1dGVCYXNlZEV4cHJlc3Npb25Tb3VyY2VWYXJpYWJsZUNvbnZlcnNpb24AAAM3NwBvAAAABSRJRAAQAAAAAK9e6uSQ4DBSu95JsrGoEdgCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACAAAABPZmZsaW5lRW50aXR5U3luY01vZGVDb252ZXJzaW9uAAADNzgAfQAAAAUkSUQAEAAAAADeIaUvadTSW6hh2ttJ2BheAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAuAAAAU291cmNlVmFyaWFibGVGb3JBc3NvY2lhdGlvbldpZGdldHNDb252ZXJzaW9uAAADNzkAdQAAAAUkSUQAEAAAAADIRw6i2AuYVp4PGaUYPDSbAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAATmF0aXZlQWNjZXNzaWJpbGl0eVNldHRpbmdzQ29udmVyc2lvbgAAAzgwAHMAAAAFJElEABAAAAAA37etmKcToFezTQukKo1nEQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJAAAAFVuc3VwcG9ydGVkRHluYW1pY0NsYXNzZXNDb252ZXJzaW9uAAADODEAaQAAAAUkSUQAEAAAAAAuq2Q+sSsmXrztunNUARerAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAaAAAAQWdncmVnYXRlQWN0aW9uQ29udmVyc2lvbgAAAzgyAHYAAAAFJElEABAAAAAA3NXnqAglX1S8JorHsdfqawIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAE5ldmVyRGVmYXVsdFBhcmFtZXRlck1hcHBpbmdDb252ZXJzaW9uAAADODMAdAAAAAUkSUQAEAAAAACx0KMSqkMSX7Rf1Ax3chRJAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAlAAAAU291cmNlVmFyaWFibGVGb3JEYXRhVmlld3NDb252ZXJzaW9uAAADODQAdQAAAAUkSUQAEAAAAAAXDnFPRnm0VYJHn/wG1gbAAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAQWJzdHJhY3REZXNpZ25Qcm9wZXJ0eVZhbHVlQ29udmVyc2lvbgAAAzg1AHIAAAAFJElEABAAAAAAuLdgFbkW4FalsYIHEMZLQQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIwAAAFNldFNuaXBwZXRTb3VyY2VWYXJpYWJsZUNvbnZlcnNpb24AAAM4NgBhAAAABSRJRAAQAAAAAOTeTey/bItVoMwFb0agcmgCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABIAAABQYWdlVXJsQ29udmVyc2lvbgAAAzg3AG4AAAAFJElEABAAAAAAHMlC/bxsf1qYPSIaIePcAAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAFVybFRyaW1TdGFydGluZ1NsYXNoQ29udmVyc2lvbgAAAzg4AHcAAAAFJElEABAAAAAAo596hA/+9lmyxEpsmq8XMQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKAAAAFNvdXJjZVZhcmlhYmxlRm9yRGVsZXRlQWN0aW9uQ29udmVyc2lvbgAAAzg5AGoAAAAFJElEABAAAAAAxWk59Gai/VqavvH2Oa3nIgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGwAAAEltYWdlQWNjZXNzUnVsZXNDb252ZXJzaW9uAAADOTAAcwAAAAUkSUQAEAAAAAD7GfKtn/9DUJ7nFyuZ9yKWAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAARGF0YWJhc2VEYXRhU291cmNlUmVtb3ZhbENvbnZlcnNpb24AAAM5MQB4AAAABSRJRAAQAAAAACgrsdh85TFchXAraSRaz80CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACkAAABEYXRhVmlld0NvbmRpdGlvbmFsRWRpdGFiaWxpdHlDb252ZXJzaW9uAAADOTIAcwAAAAUkSUQAEAAAAABQ0LcWGXHzUZX3AiYHqKQoAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAAUGxhY2Vob2xkZXJDbGllbnRUZW1wbGF0ZUNvbnZlcnNpb24AAAM5MwB6AAAABSRJRAAQAAAAAAeqXoqux11aurRwX6Lk2P0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACsAAABDbGVhbnVwRHVwbGljYXRlUGFyYW1ldGVyTWFwcGluZ0NvbnZlcnNpb24AAAM5NABvAAAABSRJRAAQAAAAAO3ya5JQIJVYjMu6kCgXP1ICJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACAAAABUYWJDb250YWluZXJBY3RpdmVUYWJDb252ZXJzaW9uAAADOTUAeAAAAAUkSUQAEAAAAAAziOEXMod7U7w/WlDhKdxgAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQApAAAAVmVzdGlnaWFsU291cmNlVmFyaWFibGVSZW1vdmFsQ29udmVyc2lvbgAAAzk2AHcAAAAFJElEABAAAAAANHe4xiEN+Vq9Vhto2tv1DQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKAAAAE5hdGl2ZVByb2ZpbGVDb29raWVFbmNyeXB0aW9uQ29udmVyc2lvbgAAAzk3AIEAAAAFJElEABAAAAAAeSsMhRgNAVW9OZ1rwOZLaQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAMgAAAFdvcmtmbG93VXNlclRhc2tPdXRjb21lUmVtb3ZlVW5pcXVlbmVzc0NvbnZlcnNpb24AAAM5OACBAAAABSRJRAAQAAAAANj/+nL5WMJXtlaZ44vw98kCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lADIAAABNaWNyb2Zsb3dMb2NrQW5kVW5sb2NrV29ya2Zsb3dTZWxlY3Rpb25Db252ZXJzaW9uAAADOTkAbwAAAAUkSUQAEAAAAAAYPW6vlbVWV4fXs1Kz298OAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAgAAAAUmVtb3ZlVW5rbm93bkxhbmd1YWdlQ29udmVyc2lvbgAAAzEwMAB6AAAABSRJRAAQAAAAALzT8ruZd5dWqnQLR5V2X4QCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACsAAABNdWx0aVVzZXJUYXNrQXNzaWduZWVBc3NvY2lhdGlvbkNvbnZlcnNpb24AAAMxMDEAbwAAAAUkSUQAEAAAAACgyK5sM7Q0XrvNhVyp3dy1AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAgAAAAVXNlclRhc2tBY3Rpdml0eVR5cGVzQ29udmVyc2lvbgAAAzEwMgB2AAAABSRJRAAQAAAAAFF8GVppGVBUmjkMe/so1/ACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACcAAABUaW1lckJvdW5kYXJ5RXZlbnRzTWV0YW1vZGVsQ29udmVyc2lvbgAAAzEwMwBmAAAABSRJRAAQAAAAAGR3LlRveLFSht7VTgV8fEYCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABcAAABQZXJzaXN0ZW50SWRDb252ZXJzaW9uAAADMTA0AHsAAAAFJElEABAAAAAAR/1OyzqURFGt+pqVbjN7jgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALAAAAEN1c3RvbVdpZGdldFJlc2V0SGlkZGVuUHJvcGVydGllc0NvbnZlcnNpb24AAAAA" + } + ] +} diff --git a/lib/mxrb/templates/project/6.10.8.json b/lib/mxrb/templates/project/6.10.8.json new file mode 100644 index 0000000..952f7a5 --- /dev/null +++ b/lib/mxrb/templates/project/6.10.8.json @@ -0,0 +1,50 @@ +{ + "format_version": "v1", + "units": [ + { + "unit_id": "4e70063d-53e1-477a-ab92-6b763330c293", + "container_id": "919afe5b-faae-41eb-9405-5ac6d4c5fc43", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Settings$ProjectSettings", + "contents": "LgcAAAUkSUQAEAAAAAA9BnBO4VN6R6uSa3YzMMKTAiRUeXBlABkAAABTZXR0aW5ncyRQcm9qZWN0U2V0dGluZ3MABFNldHRpbmdzAOEGAAAQMAACAAAAAzEAjwAAAAUkSUQAEAAAAAC82XyoYWY3WqFPOBeQlP/sAiRUeXBlAB8AAABGb3JtcyRXZWJVSVByb2plY3RTZXR0aW5nc1BhcnQAAlRoZW1lAAoAAAAoRGVmYXVsdCkACEVuYWJsZVdpZGdldEJ1bmRsaW5nAAAIRW5hYmxlRG93bmxvYWRSZXNvdXJjZXMAAAADMgBSAAAABSRJRAAQAAAAACCuBxzrK2RQpMT8mGbKGecCJFR5cGUAKAAAAFNldHRpbmdzJEludGVncmF0aW9uUHJvamVjdFNldHRpbmdzUGFydAAAAzMASQIAAAUkSUQAEAAAAADYBM3PLzOxXIxk0/GEpHcxAiRUeXBlAB8AAABTZXR0aW5ncyRDb25maWd1cmF0aW9uU2V0dGluZ3MABENvbmZpZ3VyYXRpb25zAPABAAAQMAADAAAAAzEA4QEAAAUkSUQAEAAAAACO/YWrGyHhSbiXm7cMp+LNAiRUeXBlAB0AAABTZXR0aW5ncyRTZXJ2ZXJDb25maWd1cmF0aW9uAARDdXN0b21TZXR0aW5ncwAMAAAAEDAAAwAAAAAEQ29uc3RhbnRWYWx1ZXMADAAAABAwAAMAAAAAAk5hbWUACAAAAGRlZmF1bHQAAkFwcGxpY2F0aW9uUm9vdFVybAAXAAAAaHR0cDovL2xvY2FsaG9zdDo4MDgwLwAQSHR0cFBvcnROdW1iZXIAkB8AABBTZXJ2ZXJQb3J0TnVtYmVyAJofAAAIT3Blbkh0dHBQb3J0AAAIT3BlbkFkbWluUG9ydAABEE1heEphdmFIZWFwU2l6ZQAAAAAACEVtdWxhdGVDbG91ZFNlY3VyaXR5AAECRXh0cmFKdm1QYXJhbWV0ZXJzAAEAAAAAAkRhdGFiYXNlVHlwZQAHAAAASHNxbGRiAAJEYXRhYmFzZVVybAABAAAAAAJEYXRhYmFzZU5hbWUACAAAAGRlZmF1bHQACERhdGFiYXNlVXNlSW50ZWdyYXRlZFNlY3VyaXR5AAACRGF0YWJhc2VVc2VyTmFtZQABAAAAAAJEYXRhYmFzZVBhc3N3b3JkAAEAAAAAAAAAAzQAQwEAAAUkSUQAEAAAAABlgZojVlI8U7BVMA5AmlABAiRUeXBlABcAAABTZXR0aW5ncyRNb2RlbFNldHRpbmdzAAJBZnRlclN0YXJ0dXBNaWNyb2Zsb3cAAQAAAAACQmVmb3JlU2h1dGRvd25NaWNyb2Zsb3cAAQAAAAACSGVhbHRoQ2hlY2tNaWNyb2Zsb3cAAQAAAAACRmlyc3REYXlPZldlZWsACAAAAERlZmF1bHQAAkRlZmF1bHRUaW1lWm9uZUNvZGUAAQAAAAACU2NoZWR1bGVkRXZlbnRUaW1lWm9uZUNvZGUACAAAAEV0Yy9VVEMAAkhhc2hBbGdvcml0aG0ABwAAAEJDcnlwdAACUm91bmRpbmdNb2RlAAcAAABIYWxmVXAACEFsbG93VXNlck11bHRpcGxlU2Vzc2lvbnMAAQADNQBkAAAABSRJRAAQAAAAAHcOIxg+deRbn5o+ghgleSkCJFR5cGUAHAAAAFNldHRpbmdzJENvbnZlbnRpb25TZXR0aW5ncwAITG93ZXJDYXNlTWljcm9mbG93VmFyaWFibGVzAAAAAzYAIwEAAAUkSUQAEAAAAADDsyqg03mLW6xJOo+KaIpQAiRUeXBlABoAAABTZXR0aW5ncyRMYW5ndWFnZVNldHRpbmdzAARMYW5ndWFnZXMAtQAAABAwAAMAAAADMQCmAAAABSRJRAAQAAAAAHSyEuKfy61JvcsgSwDWr8UCJFR5cGUADwAAAFRleHRzJExhbmd1YWdlAAJDb2RlAAYAAABlbl9VUwAIQ2hlY2tDb21wbGV0ZW5lc3MAAAJDdXN0b21EYXRlRm9ybWF0AAEAAAAAAkN1c3RvbVRpbWVGb3JtYXQAAQAAAAACQ3VzdG9tRGF0ZVRpbWVGb3JtYXQAAQAAAAAAAAJEZWZhdWx0TGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwAAAzcAYQAAAAUkSUQAEAAAAACEUrRNWZ4FVZVUbGnMf2GOAiRUeXBlAB0AAABTZXR0aW5ncyRDZXJ0aWZpY2F0ZVNldHRpbmdzAARDZXJ0aWZpY2F0ZXMADAAAABAwAAMAAAAAAAM4AGgAAAAFJElEABAAAAAA9WkgnFcd0Fy3SoSqiMzbyAIkVHlwZQAdAAAAU2V0dGluZ3MkSmF2YUFjdGlvbnNTZXR0aW5ncwAIR2VuZXJhdGVQb3N0Zml4ZXNGb3JQYXJhbWV0ZXJzAAEAAAA=" + }, + { + "unit_id": "b62986ab-1ec9-5c84-9d33-1f0829ffce9b", + "container_id": "919afe5b-faae-41eb-9405-5ac6d4c5fc43", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Navigation$NavigationDocument", + "contents": "uQgAAAUkSUQAEAAAAACrhim2yR6EXJ0zHwgp/86bAiRUeXBlAB4AAABOYXZpZ2F0aW9uJE5hdmlnYXRpb25Eb2N1bWVudAADUGhvbmVQcm9maWxlAFYBAAAFJElEABAAAAAAwq7FcljiZ1mgRfuq+YfEXQIkVHlwZQAdAAAATmF2aWdhdGlvbiROYXZpZ2F0aW9uUHJvZmlsZQADSG9tZVBhZ2UAWQAAAAUkSUQAEAAAAAAnNVtHHZKmXaXMdXn19/YXAiRUeXBlABQAAABOYXZpZ2F0aW9uJEhvbWVQYWdlAAJQYWdlAAEAAAAAAk1pY3JvZmxvdwABAAAAAAAESG9tZUl0ZW1zAAwAAAAQMAACAAAAAANNZW51AFYAAAAFJElEABAAAAAAd6JQEHAXTVKuQ5FffbPVaQIkVHlwZQAZAAAATWVudXMkTWVudUl0ZW1Db2xsZWN0aW9uAARJdGVtcwAMAAAAEDAAAwAAAAAACEVuYWJsZWQAAAhPZmZsaW5lRW5hYmxlZDYAAAJBcHBsaWNhdGlvblRpdGxlAAcAAABNZW5kaXgAAANUYWJsZXRQcm9maWxlAFYBAAAFJElEABAAAAAA7vsIrF66/1uD1c0S98S0qgIkVHlwZQAdAAAATmF2aWdhdGlvbiROYXZpZ2F0aW9uUHJvZmlsZQADSG9tZVBhZ2UAWQAAAAUkSUQAEAAAAABf606KM+cWSL+OS7ejPfbCAiRUeXBlABQAAABOYXZpZ2F0aW9uJEhvbWVQYWdlAAJQYWdlAAEAAAAAAk1pY3JvZmxvdwABAAAAAAAESG9tZUl0ZW1zAAwAAAAQMAACAAAAAANNZW51AFYAAAAFJElEABAAAAAAvv9h6oaTOUSVEDoI1k4Q5gIkVHlwZQAZAAAATWVudXMkTWVudUl0ZW1Db2xsZWN0aW9uAARJdGVtcwAMAAAAEDAAAwAAAAAACEVuYWJsZWQAAAhPZmZsaW5lRW5hYmxlZDYAAAJBcHBsaWNhdGlvblRpdGxlAAcAAABNZW5kaXgAAANEZXNrdG9wUHJvZmlsZQBWAQAABSRJRAAQAAAAAC8EoDhgSBlTtPL1Y2hswrwCJFR5cGUAHQAAAE5hdmlnYXRpb24kTmF2aWdhdGlvblByb2ZpbGUAA0hvbWVQYWdlAFkAAAAFJElEABAAAAAA8GMrXNYiyla/GJmZ9UwIcQIkVHlwZQAUAAAATmF2aWdhdGlvbiRIb21lUGFnZQACUGFnZQABAAAAAAJNaWNyb2Zsb3cAAQAAAAAABEhvbWVJdGVtcwAMAAAAEDAAAgAAAAADTWVudQBWAAAABSRJRAAQAAAAAO9qmxXD2yNbpzXPmfg0F3ECJFR5cGUAGQAAAE1lbnVzJE1lbnVJdGVtQ29sbGVjdGlvbgAESXRlbXMADAAAABAwAAMAAAAAAAhFbmFibGVkAAEIT2ZmbGluZUVuYWJsZWQ2AAACQXBwbGljYXRpb25UaXRsZQAHAAAATWVuZGl4AAADT2ZmbGluZVBob25lUHJvZmlsZQBWAQAABSRJRAAQAAAAABfet6qbLmZUtfK3+XLF8RMCJFR5cGUAHQAAAE5hdmlnYXRpb24kTmF2aWdhdGlvblByb2ZpbGUAA0hvbWVQYWdlAFkAAAAFJElEABAAAAAA5pZWBPCeAFWCOlwaGYT2+wIkVHlwZQAUAAAATmF2aWdhdGlvbiRIb21lUGFnZQACUGFnZQABAAAAAAJNaWNyb2Zsb3cAAQAAAAAABEhvbWVJdGVtcwAMAAAAEDAAAgAAAAADTWVudQBWAAAABSRJRAAQAAAAAKjU/XO8xQ9btrPm+u+kmU4CJFR5cGUAGQAAAE1lbnVzJE1lbnVJdGVtQ29sbGVjdGlvbgAESXRlbXMADAAAABAwAAMAAAAAAAhFbmFibGVkAAAIT2ZmbGluZUVuYWJsZWQ2AAACQXBwbGljYXRpb25UaXRsZQAHAAAATWVuZGl4AAADSHlicmlkUGhvbmVQcm9maWxlNgBWAQAABSRJRAAQAAAAAMgOGFGn1RNfm7J9Ex/WiNMCJFR5cGUAHQAAAE5hdmlnYXRpb24kTmF2aWdhdGlvblByb2ZpbGUAA0hvbWVQYWdlAFkAAAAFJElEABAAAAAAKK4bEnwYQVO7xGjOHCTOVwIkVHlwZQAUAAAATmF2aWdhdGlvbiRIb21lUGFnZQACUGFnZQABAAAAAAJNaWNyb2Zsb3cAAQAAAAAABEhvbWVJdGVtcwAMAAAAEDAAAgAAAAADTWVudQBWAAAABSRJRAAQAAAAAI4ZrVtT5epemHhvcpwV190CJFR5cGUAGQAAAE1lbnVzJE1lbnVJdGVtQ29sbGVjdGlvbgAESXRlbXMADAAAABAwAAMAAAAAAAhFbmFibGVkAAAIT2ZmbGluZUVuYWJsZWQ2AAACQXBwbGljYXRpb25UaXRsZQAHAAAATWVuZGl4AAADSHlicmlkVGFibGV0UHJvZmlsZTYAVgEAAAUkSUQAEAAAAADY/c1FNes5XrUn4A0IKf2AAiRUeXBlAB0AAABOYXZpZ2F0aW9uJE5hdmlnYXRpb25Qcm9maWxlAANIb21lUGFnZQBZAAAABSRJRAAQAAAAAD8RLt1vp7datuBKH3JrB4ECJFR5cGUAFAAAAE5hdmlnYXRpb24kSG9tZVBhZ2UAAlBhZ2UAAQAAAAACTWljcm9mbG93AAEAAAAAAARIb21lSXRlbXMADAAAABAwAAIAAAAAA01lbnUAVgAAAAUkSUQAEAAAAABm5EvsYWlLVqXL5PsHbSv9AiRUeXBlABkAAABNZW51cyRNZW51SXRlbUNvbGxlY3Rpb24ABEl0ZW1zAAwAAAAQMAADAAAAAAAIRW5hYmxlZAAACE9mZmxpbmVFbmFibGVkNgAAAkFwcGxpY2F0aW9uVGl0bGUABwAAAE1lbmRpeAAAAA==" + }, + { + "unit_id": "57d2ecb5-6c2f-4ffa-a0b3-a37a40da3adf", + "container_id": "919afe5b-faae-41eb-9405-5ac6d4c5fc43", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Texts$SystemTextCollection", + "contents": "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aGFzIGV4cGlyZWQuIFBsZWFzZSBlbnRlciB5b3VyIHVzZXIgbmFtZSBhbmQgcGFzc3dvcmQgdG8gY29udGludWUgd29ya2luZy4AAAMyALIAAAAFJElEABAAAAAAlIzResy1IVeF4MizC44PcgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAVAAAAFV3IHNlc3NpZSBpcyB2ZXJsb3Blbi4gVm9lciB1dyBnZWJydWlrZXJzbmFhbSBlbiB3YWNodHdvb3JkIGluIG9tIHZlcmRlciB0ZSB3ZXJrZW4uAAAAAAJJbnRlcm5hbEtleQAlAAAAbWVuZGl4LmNvcmUuTXhTZXJ2ZXIuaW52YWxpZF9zZXNzaW9uAAADNDkA8QEAAAUkSUQAEAAAAAD746UeVn7bSLd6wYkw4tOwAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AANUZXh0AIkBAAAFJElEABAAAAAAiP5HKiPRTkK+Gkayimh/6QIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMATQEAABAwAAMAAAADMQCYAAAABSRJRAAQAAAAAKbu8O+xLD1ainqonkE3+XUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADoAAABUaGUgY3VycmVudCBsaWNlbnNlIGRvZXMgbm90IGFsbG93IG1vcmUgdXNlcnMgdG8gc2lnbiBpbi4AAAMyAKMAAAAFJElEABAAAAAAlGrKkteI9VCX6zRaNirjswIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQARQAAAERlIGh1aWRpZ2UgbGljZW50aWUgc3RhYXQgbmlldCB0b2UgZGF0IGVyIG1lZXIgZ2VicnVpa2VycyBhYW5tZWxkZW4uAAAAAAJJbnRlcm5hbEtleQAWAAAAbWVuZGl4LnNpZ25pbi5odHRwNDAyAAADNTAAlAEAAAUkSUQAEAAAAAAGEb9GotcXVI3aQ+g9KxctAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AANUZXh0ACEBAAAFJElEABAAAAAAnYM+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VHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMALAEAABAwAAMAAAADMQCOAAAABSRJRAAQAAAAABYHXdllc8hZoBgi/Z+9EPICJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADAAAABQYXNzd29yZCBzaG91bGQgY29udGFpbiBhbiB1cHBlcmNhc2UgY2hhcmFjdGVyLgAAAzIAjAAAAAUkSUQAEAAAAACc+kcKdR+OV62Mg6XlzWBfAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAuAAAASGV0IHdhY2h0d29vcmQgbW9ldCBlZW4gaG9vZmRsZXR0ZXIgYmV2YXR0ZW4uAAAAAAJJbnRlcm5hbEtleQAhAAAAbWVuZGl4LnBsYXRmb3JtLm1pc3NpbmdVcHBlcmNhc2UAAAM3MQDcAQAABSRJRAAQAAAAAGfxSgiWVRxYgP1LURpLBc8CJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAA1RleHQAaQEAAAUkSUQAEAAAAAB0gLDCTtdNVr+NTjOLrEZtAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAtAQAAEDAAAwAAAAMxAI0AAAAFJElEABAAAAAAGu6SRDtlvl+q+lRwwLPbjQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALwAAAFBhc3N3b3JkIHNob3VsZCBjb250YWluIGEgbG93ZXJjYXNlIGNoYXJhY3Rlci4AAAMyAI4AAAAFJElEABAAAAAAN5JEV7am0lSrFM6SYsHrgQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAMAAAAEhldCB3YWNodHdvb3JkIG1vZXQgZWVuIGtsZWluZSBsZXR0ZXIgYmV2YXR0ZW4uAAAAAAJJbnRlcm5hbEtleQAhAAAAbWVuZGl4LnBsYXRmb3JtLm1pc3NpbmdMb3dlcmNhc2UAAAM3MgAvAgAABSRJRAAQAAAAAM4c+X6jCNRVlz/JHIQDlH8CJFR5cGUA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AACAcPMVT4rmW6SGvEIoJORrAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAwAQAAEDAAAwAAAAMxAI0AAAAFJElEABAAAAAAjq9+2qxTr1mO8W35TcSfkwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALwAAAEFyZSB5b3Ugc3VyZSB5b3Ugd2FudCB0byBjbGVhciB0aGlzIHJlZmVyZW5jZT8AAAMyAJEAAAAFJElEABAAAAAALa/ZJOOmFVmyXb0e1OxNewIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAMwAAAFdlZXQgdSB6ZWtlciBkYXQgdSBkZXplIHZlcndpanppbmcgd2lsdCBsZWVnbWFrZW4/AAAAAAJJbnRlcm5hbEtleQAzAAAAbW9iaWxlLndpZGdldC5SZWZlcmVuY2VTZWxlY3Rvci5jbGVhbl9jb25maXJtYXRpb24AAAM3NwDwAQAABSRJRAAQAAAAAFId76OL869SjU+LxsPj28oCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAA1RleHQAcgEAAAUkSUQAEAAAAABhzssWh3lIW7vc1z97KoCMAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwA2AQAAEDAAAwAAAAMxAJMAAAAFJElEABAAAAAA+WnUnwr6O1W4Qh9MspIHSwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQANQAAAFRhYiB7MX0gY29udGFpbnMgdmFsaWRhdGlvbiBlcnJvcnMgYnV0IGlzIGludmlzaWJsZS4AAAMyAJEAAAAFJElEABAAAAAANlUf/7gtBFWTVe7N3T0DSAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAMwAAAFRhYiB7MX0gYmV2YXQgdmFsaWRhdGllZm91dGVuIG1hYXIgaXMgb256aWNodGJhYXIuAAAAAAJJbnRlcm5hbEtleQAsAAAAbWVuZGl4LndpZGdldC5UYWJDb250ZW50LmhpZGRlbl92YWxpZGF0aW9ucwAAAzc4APkBAAAFJElEABAAAAAATKnWknZEElG5qTjDis44KgIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAADVGV4dACKAQAABSRJRAAQAAAAAFgCDD05KwpcvbAEdWnnitsCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAE4BAAAQMAADAAAAAzEAmgAAAAUkSUQAEAAAAABQ932XzCLvX7EpXqppeBrHAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAA8AAAAQW4gZXJyb3Igb2NjdXJlZCwgcGxlYXNlIGNvbnRhY3QgeW91ciBzeXN0ZW0gYWRtaW5pc3RyYXRvci4AAAMyAKIAAAAFJElEABAAAAAA7ZnPzeP0QVCGOiViyDkmEwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQARAAAAEVyIGlzIGVlbiBmb3V0IG9wZ2V0cmVkZW4sIG5lZW0gY29udGFjdCBvcCBtZXQgdXcgc3lzdGVlbWJlaGVlcmRlci4AAAAAAkludGVybmFsS2V5AB0AAABtb2JpbGUuc3lzLlVJLmludGVybmFsX2Vycm9yAAADNzkArAEAAAUkSUQAEAAAAACItianTcbZVZVI0oGALtVvAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AANUZXh0ADQBAAAFJElEABAAAAAADWXX86MjY1am22JZWrfOSwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA+AAAABAwAAMAAAADMQBxAAAABSRJRAAQAAAAAJU5/ArQJ4hdsqbJdlkqa4oCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ABMAAAB7MX0gaXRlbXMgc2VsZWN0ZWQAAAMyAHUAAAAFJElEABAAAAAARWiCI42jAV2Xy0ecLZkphAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAFwAAAHsxfSBpdGVtcyBnZXNlbGVjdGVlcmQAAAAAAkludGVybmFsS2V5ACYAAABtZW5kaXgud2lkZ2V0LlNlbGVjdEJveC5pdGVtc1NlbGVjdGVkAAADODAAGwIAAAUkSUQAEAAAAACdzz87MeOCVrOXnb9hiTxdAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AANUZXh0ALMBAAAFJElEABAAAAAAwyvu3nU3NlO6iOuUBobGswIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAdwEAABAwAAMAAAADMQC2AAAABSRJRAAQAAAAAGwQCH08VmZaq6xyHhJ1D28CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AFgAAABZb3VyIHNlc3Npb24gaGFzIGV4cGlyZWQuIFBsZWFzZSBlbnRlciB5b3VyIHVzZXIgbmFtZSBhbmQgcGFzc3dvcmQgdG8gY29udGludWUgd29ya2luZy4AAAMyAK8AAAAFJElEABAAAAAA/hUrKjDYo1SvDKd48Arh8gIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAUQAAAFV3IHNlc3NpZSBpcyB2ZXJsb3Blbi4gVnVsIHV3IGdlYnJ1aWtlcnNuYWFtIGVuIHdhY2h0d29vcmQgaW4gb20gdmVyZGVyIHRlIGdhYW4uAAAAAAJJbnRlcm5hbEtleQAWAAAAbWVuZGl4LnNpZ25pbi5odHRwNDE5AAADODEA7wEAAAUkSUQAEAAAAACeJ3f53OtEVINiAYqD8ES1AiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AANUZXh0AH4BAAAFJElEABAAAAAAxVQoVeCe/12aUFkn/pu73gIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAQgEAABAwAAMAAAADMQCUAAAABSRJRAAQAAAAAGzCpcP8TBRQoV2yzrWKg8oCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADYAAABUaGlzIHZhbHVlIHNob3VsZCBiZSBzbWFsbGVyIHRoYW4gOTIyMzM3MjAzNjg1NDc3NTgwNwAAAzIAnAAAAAUkSUQAEAAAAAD+z9bK9GURVosI5jDeR3xuAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAA+AAAARGUgb3BnZWdldmVuIHdhYXJkZSBtb2V0IGtsZWluZXIgemlqbiBkYW4gOTIyMzM3MjAzNjg1NDc3NTgwNwAAAAACSW50ZXJuYWxLZXkAHwAAAG1lbmRpeC5wbGF0Zm9ybS5sb25nX2Fib3ZlX21heAAAAzgyAO8BAAAFJElEABAAAAAAiRbkzNwUvFGzKJL2cIADAAIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAADVGV4dAB+AQAABSRJRAAQAAAAAMJfnRI5F3pRo9ynILiqtxsCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAEIBAAAQMAADAAAAAzEAlAAAAAUkSUQAEAAAAACVEgmtJdRoW7DkDcO1KVAuAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAA2AAAAVGhpcyB2YWx1ZSBzaG91bGQgYmUgbGFyZ2VyIHRoYW4gLTkyMjMzNzIwMzY4NTQ3NzU4MDgAAAMyAJwAAAAFJElEABAAAAAAYytQ+h99DFucgkOqDBox9wIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAPgAAAERlIG9wZ2VnZXZlbiB3YWFyZGUgbW9ldCBncm90ZXIgemlqbiBkYW4gLTkyMjMzNzIwMzY4NTQ3NzU4MDgAAAAAAkludGVybmFsS2V5AB8AAABtZW5kaXgucGxhdGZvcm0ubG9uZ19iZWxvd19taW4AAAM4MwBDAgAABSRJRAAQAAAAAJfIzZD6DdVWrP77nAP04mICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAA1RleHQAzAEAAAUkSUQAEAAAAAACCWE6q2D9U4oGu9bDjWQ2AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwCQAQAAEDAAAwAAAAMxAL4AAAAFJElEABAAAAAAMPlY/GbnnVCqyn/s7Hl6+gIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAYAAAAERlY2ltYWwgdmFsdWVzIG1heSBjb250YWluIG5vIG1vcmUgdGhhbiAyMCBkaWdpdHMgYmVmb3JlIGFuZCA4IGRpZ2l0cyBiZWhpbmQgdGhlIGRlY2ltYWwgcG9pbnQuAAADMgDAAAAABSRJRAAQAAAAAFT9sqxahNNVrh0bEiFowdACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AGIAAABEZWNpbWFsZSBnZXRhbGxlbiBtb2V0ZW4gbWluZGVyIGdldGFsbGVuIGJldmF0dGVuIGRhbiAyMCBkZWNpbWFsZW4gdm9vciBlbiA4IGdldGFsbGVuIG5hIGRlIHB1bnQuAAAAAAJJbnRlcm5hbEtleQAlAAAAbWVuZGl4LmxpYi5WYWxpZGF0aW9ucy5yYW5nZV9kZWNpbWFsAAADODQAAgIAAAUkSUQAEAAAAABoTwJgJAj/X6VkRi9znIoYAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AANUZXh0AIwBAAAFJElEABAAAAAAEF2kKgb2R1Cyx5BCTxjYIwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAUAEAABAwAAMAAAADMQCbAAAABSRJRAAQAAAAAGUxP6x2pN1Xjlt8WfsklgwCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AD0AAABUaGlzIG51bWJlciBpcyB0b28gYmlnIGFuZCBjYW5ub3QgYmUgYWNjdXJhdGVseSByZXByZXNlbnRlZC4AAAMyAKMAAAAFJElEABAAAAAApUBZmgD1S1ug9m3NY3QeUwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQARQAAAERpdCBnZXRhbCBpcyB0ZSBncm9vdCBlbiBrYW4gbmlldCBuYXV3a2V1cmlnIHdvcmRlbiBnZXJlcHJlc2VudGVlcmQuAAAAAAJJbnRlcm5hbEtleQAkAAAAbWVuZGl4LmxpYi5WYWxpZGF0aW9ucy5yYW5nZV9udW1i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biBnZXN5bmNocm9uaXNlZXJkLgAAAAACSW50ZXJuYWxLZXkAGQAAAG1vYmlsZS5zeXMuVUkuc3luY19lcnJvcgAAAzkxAKIBAAAFJElEABAAAAAAQ7viYGemjl2OWRUpPTyr0QIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAADVGV4dAA1AQAABSRJRAAQAAAAAJ7b4mv0xstTv8fa6E8//voCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAPkAAAAQMAADAAAAAzEAbwAAAAUkSUQAEAAAAAAJgHQEWtwOWLY+GnZX0CwnAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAARAAAAQmFjayB0byBIb21lcGFnZQAAAzIAeAAAAAUkSUQAEAAAAACi785E2/i4U7rY3QFVlA0nAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAaAAAAVGVydWcgbmFhciBkZSBzdGFydHBhZ2luYQAAAAACSW50ZXJuYWxLZXkAGwAAAG1lbmRpeC5zaWduaW4uZ29Ib21lQnV0dG9uAAADOTIAQAIAAAUkSUQAEAAAAAC7EQvV6KhfXYCSX5bCMBqiAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AANUZXh0ANgBAAAFJElEABAAAAAAwVc60Cc5Bl6UDYtEHA1/KwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAnAEAABAwAAMAAAADMQC/AAAABSRJRAAQAAAAAMGPYVzZ7+ZSuzYk0B5rMIUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AGEAAABZb3UgZG9uJ3QgaGF2ZSBlbm91Z2ggcGVybWlzc2lvbnMgdG8gYWNjZXNzIHRoaXMgcGFnZS4gWW91IG1heSB0cnkgdG8gbG9naW4gYXMgYSBkaWZmZXJlbnQgdXNlci4AAAMyAMsAAAAFJElEABAAAAAA2xE0HiEtHFyLgjalaMe3twIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAbQAAAFUgaGVidCBuaWV0IGdlbm9lZyByZWNodGVuIG9tIGRlemUgcGFnaW5hIHRlIG9wZW5lbi4gVSBrdW50IHByb2JlcmVuIGFhbiB0ZSBtZWxkZW4gYWxzIGVlbiBhbmRlcmUgZ2VicnVpa2VyLgAAAAACSW50ZXJuYWxLZXkAFgAAAG1lbmRpeC5zaWduaW4uaHR0cDQwMwAAAAA=" + }, + { + "unit_id": "bd6f65bd-2393-539d-9388-4684a54d0646", + "container_id": "919afe5b-faae-41eb-9405-5ac6d4c5fc43", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Security$ProjectSecurity", + "contents": "VAQAAAUkSUQAEAAAAAC9ZW+9kyOdU5OIRoSlTQZGAiRUeXBlABkAAABTZWN1cml0eSRQcm9qZWN0U2VjdXJpdHkAA0ZpbGVEb2N1bWVudEFjY2VzcwBsAAAABSRJRAAQAAAAAAh2641J4YpGn/gkNTbQ268CJFR5cGUAKQAAAFNlY3VyaXR5JEZpbGVEb2N1bWVudEFjY2Vzc1J1bGVDb250YWluZXIABEFjY2Vzc1J1bGVzAAwAAAAQMAADAAAAAAADSW1hZ2VBY2Nlc3MAZQAAAAUkSUQAEAAAAABRZ3yQBxdRRKLK3P7Cb5Y+AiRUeXBlACIAAABTZWN1cml0eSRJbWFnZUFjY2Vzc1J1bGVDb250YWluZXIABEFjY2Vzc1J1bGVzAAwAAAAQMAADAAAAAAADUGFzc3dvcmRQb2xpY3lTZXR0aW5ncwCPAAAABSRJRAAQAAAAAP19SHgAaVBFuvXItKSIgrgCJFR5cGUAIAAAAFNlY3VyaXR5JFBhc3N3b3JkUG9saWN5U2V0dGluZ3MAEE1pbmltdW1MZW5ndGgABgAAAAhSZXF1aXJlTWl4ZWRDYXNlAAEIUmVxdWlyZVN5bWJvbAAACFJlcXVpcmVEaWdpdAABAARVc2VyUm9sZXMADAAAABAwAAIAAAAABERlbW9Vc2VycwCSAQAAEDAAAgAAAAMxAMkAAAAFJElEABAAAAAAD8QgBhnLAECUGViBCTkP7gIkVHlwZQAWAAAAU2VjdXJpdHkkRGVtb1VzZXJJbXBsAAJVc2VyTmFtZQATAAAAZGVtb19hZG1pbmlzdHJhdG9yAAJQYXNzd29yZAALAAAAV1BWUUtUdFQzOQACRW50aXR5ABcAAABBZG1pbmlzdHJhdGlvbi5BY2NvdW50AARVc2VyUm9sZXMAIQAAABAwAAEAAAACMQAOAAAAQWRtaW5pc3RyYXRvcgAAAAMyALcAAAAFJElEABAAAAAAZT44j85+VEeIrehkQObKCgIkVHlwZQAWAAAAU2VjdXJpdHkkRGVtb1VzZXJJbXBsAAJVc2VyTmFtZQAKAAAAZGVtb191c2VyAAJQYXNzd29yZAALAAAAa3RLc3hzSXQyOQACRW50aXR5ABcAAABBZG1pbmlzdHJhdGlvbi5BY2NvdW50AARVc2VyUm9sZXMAGAAAABAwAAEAAAACMQAFAAAAVXNlcgAAAAACU2VjdXJpdHlMZXZlbAANAAAAQ2hlY2tOb3RoaW5nAAhDaGVja1NlY3VyaXR5AAECQWRtaW5Vc2VyTmFtZQAIAAAATXhBZG1pbgACQWRtaW5QYXNzd29yZAACAAAAMQACQWRtaW5Vc2VyUm9sZQABAAAAAAhFbmFibGVEZW1vVXNlcnMAAQhFbmFibGVHdWVzdEFjY2VzcwAAAkd1ZXN0VXNlclJvbGUAAQAAAAACU2lnbkluTWljcm9mbG93AAEAAAAAAA==" + }, + { + "unit_id": "1fcc2823-c303-5a27-b6c7-78b04c96c88f", + "container_id": "919afe5b-faae-41eb-9405-5ac6d4c5fc43", + "module": null, + "containment": "ProjectConversion", + "name": "", + "type": "Projects$ProjectConversion", + "contents": "7BUAAAUkSUQAEAAAAAAjKMwfA8MnWrbHeLBMlsiPAiRUeXBlABsAAABQcm9qZWN0cyRQcm9qZWN0Q29udmVyc2lvbgAET25lVGltZUNvbnZlcnNpb25zAJMVAAAQMAADAAAAAzEAagAAAAUkSUQAEAAAAACztnILJCKkU5tG+C7TKnDhAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAbAAAAVW5pZmllZENvbmRpdGlvbkNvbnZlcnNpb24AAAMyAG4AAAAFJElEABAAAAAAkn6YPr/hf1aHzhumV0VGEAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAE1vdmVTZXR0aW5nc1RvUGFydHNDb252ZXJzaW9ucwAAAzMAagAAAAUkSUQAEAAAAABgh+VGzLKwX4DHvEWFciIEAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAbAAAARGF0YVR5cGVUb014VHlwZUNvbnZlcnNpb24AAAM0AHQAAAAFJElEABAAAAAAXXWCgmqgaFKxJ7O8eLTowAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAFBhcmFtZXRlck14VHlwZVRvQmFzaWNUeXBlQ29udmVyc2lvbgAAAzUAfgAAAAUkSUQAEAAAAABKZ01/le4vXbilsr77D8nUAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAvAAAAUGFyYW1ldGVyTWFwcGluZ0FyZ3VtZW50VG9CYXNpY1ZhbHVlQ29udmVyc2lvbgAAAzYAcwAAAAUkSUQAEAAAAACFJLvAkCuXW7gDt7doEgrzAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAAQ29udmVydExvY2FsaXplZENvbnRlbnRzQW5kTG9jYXRpb24AAAM3AH4AAAAFJElEABAAAAAAQC31DeixhVCjVAF6Nu4QugIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALwAAAENvbnZlcnRJbmhlcml0YW5jZVBhcmFtZXRlck5hbWVJbkV4cG9ydE1hcHBpbmcAAAM4AHkAAAAFJElEABAAAAAAlOg4LAcMMF2kDbxlFRQHMwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKgAAAENvbnZlcnRJbmhlcml0YW5jZUNvbXBsZXhQYXJhbWV0ZXJNYXBwaW5nAAADOQBmAAAABSRJRAAQAAAAADvHejErZtNbixKrYAm9UmMCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABcAAABDb252ZXJ0SW5oZXJpdGFuY2VQYXRoAAADMTAAewAAAAUkSUQAEAAAAADBWkQc1L01VLAauBV2iGw5AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAsAAAAQ29udmVydEV4cG9ydE1hcHBpbmdGb3JDaG9pY2VPYmplY3RIYW5kbGluZwAAAzExAG4AAAAFJElEABAAAAAAUe6JZK21dFirTJkoM9E2wwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAENvbnZlcnRDaG9pY2VFbGVtZW50T2NjdXJyZW5jZQAAAzEyAHMAAAAFJElEABAAAAAAy5VBvn57glGzQWdX9RbT/wIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJAAAAENvbnZlcnRJbmhlcml0YW5jZUVsZW1lbnRPY2N1cnJlbmNlAAADMTMAZwAAAAUkSUQAEAAAAABtSRAetvykXbb4VXJLxa2PAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAYAAAAQ29udmVydFhtbFByaW1pdGl2ZVR5cGUAAAMxNAB0AAAABSRJRAAQAAAAAP4Tc7jqvKFSnteyVNzlmpUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACUAAABDb252ZXJ0WG1sUHJpbWl0aXZlVHlwZUludGVnZXJUb0xvbmcAAAMxNQB0AAAABSRJRAAQAAAAAF4QsqQGWahbn6ZYPAhKMooCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACUAAABDb252ZXJ0Q2hvaWNlSW5oZXJpdGFuY2VBc3NvY2lhdGlvbnMAAAMxNgBkAAAABSRJRAAQAAAAANyO9NTnOHNRtZCuIfUocbsCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABUAAABDb252ZXJ0SXNEZWZhdWx0VHlwZQAAAzE3AHQAAAAFJElEABAAAAAAombhe2GzMFeTRa+UbzG86gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAENvbnZlcnRFeHBlY3RlZENvbnRlbnRUeXBlc1RvVXNlTXRvbQAAAzE4AGYAAAAFJElEABAAAAAAEvBNl+oCU1yxesjj/v680AIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFwAAAENvbnZlcnRYbWxFeHBvc2VkTmFtZXMAAAMxOQBrAAAABSRJRAAQAAAAAMzM8qMFULVRj9ofSq7/39kCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABDb252ZXJ0SnNvbkVsZW1lbnRzT3B0aW9uYWwAAAMyMABrAAAABSRJRAAQAAAAAKBt0uB7nDRZgZH5JnYDZE4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABDb252ZXJ0SnNvbkVsZW1lbnRzTmlsbGFibGUAAAMyMQBmAAAABSRJRAAQAAAAAEe8IsWvh+1av5Yg+Z4nXoACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABcAAABDb252ZXJ0TnVsbFZhbHVlT3B0aW9uAAADMjIAbAAAAAUkSUQAEAAAAACfseM1jcTzUZvFxOlGmdxlAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAdAAAAQ29udmVydFN5c3RlbUlkRGF0YUF0dHJpYnV0ZQAAAzIzAGUAAAAFJElEABAAAAAAwWdANFYgLVauwuFriq2qcgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFgAAAE9mZmxpbmVQcm9maWxlSURGaXhlcgAAAzI0AGUAAAAFJElEABAAAAAAtbzo+PdwlFWC4gSYLZ5sHgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFgAAAExhYmVsVG9UZXh0Q29udmVyc2lvbgAAAzI1AG4AAAAFJElEABAAAAAA0bYebsL801S73qWMBj/iCAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAEJ1dHRvbkNsaWVudFRlbXBsYXRlQ29udmVyc2lvbgAAAzI2AGkAAAAFJElEABAAAAAAxEH9QE7GoFGJ73kUs/StpQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGgAAAEdyb3VwQm94Q2FwdGlvbkNvbnZlcnNpb24AAAMyNwB8AAAABSRJRAAQAAAAANqHfOCn7i5SsqphiNiHlfACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAC0AAABSZW1vdmVHcmlkTmV3QnV0dG9uUGFnZVNldHRpbmdzV2hlbk5vdE5lZWRlZAAAAzI4AGUAAAAFJElEABAAAAAADttjGYNigF2IDIbEh7e7+QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFgAAAEZpeER1cGxpY2F0ZUFyZ3VtZW50cwAAAzI5AG0AAAAFJElEABAAAAAAuGy+KOULk1Of/i9dVNUKDwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHgAAAFNwbGl0TGlzdFZpZXdTZWFyY2hDb252ZXJzaW9uAAADMzAAbgAAAAUkSUQAEAAAAAAjTs88Za5oWYbjxL9JvuWRAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAfAAAARXh0ZW5kc0dyaWRTZWFyY2hCYXJDb252ZXJzaW9uAAADMzEAcgAAAAUkSUQAEAAAAAD3qJuhKW6NX5FrRLQLP/ojAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAjAAAAQ2xlYXJJbnZpc2libGVTZWFyY2hCYXJzQ29udmVyc2lvbgAAAzMyAGsAAAAFJElEABAAAAAAvyXQLKhn8VWbsU38EVVu7gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAE5ld0J1dHRvbkVudGl0eVRvRW50aXR5UGF0aAAAAzMzAHUAAAAFJElEABAAAAAAdpWXrFfGp1+VhgOcC3guNAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJgAAAFNldERhdGFWaWV3Q29udHJvbEJhclRvTnVsbENvbnZlcnNpb24AAAMzNAB0AAAABSRJRAAQAAAAAK/PiY3C+XJZjDTzqx4Jm3ACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACUAAABEYXRhVmlld0NvbnRyb2xCYXJUb0Zvb3RlckNvbnZlcnNpb24AAAMzNQBtAAAABSRJRAAQAAAAABTelvwqGrhWnzCXYGrz9b8CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB4AAABMYXlvdXRDYWxsT3B0aW9uYWxDb252ZXJzaW9uIAAAAzM2AHcAAAAFJElEABAAAAAARqxZR/9BqlShICYfhTr2lQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKAAAAFBsYWNlaG9sZGVySWRlbnRpZmllcnNUb05hbWVzQ29udmVyc2lvbgAAAzM3AHYAAAAFJElEABAAAAAA115YfHtdwFqVAhrABffugQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAExheW91dENhbGxBcmd1bWVudElkZW50aWZpZXJDb252ZXJzaW9uAAADMzgAdQAAAAUkSUQAEAAAAAApoQKXwFKOVa2EIQ7z7uVHAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAUmVtb3ZlRHVwbGljYXRlVHJhbnNsYXRpb25zQ29udmVyc2lvbgAAAzM5AGwAAAAFJElEABAAAAAAhv6T0OWCG1CA7yQus9LObwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHQAAAEphdmFBY3Rpb25TZXR0aW5nc0NvbnZlcnNpb24AAAM0MAB3AAAABSRJRAAQAAAAAJom5Fei2HNbo1oqaktLATUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACgAAABDb252ZXJ0UmVzdENhbGxBY3Rpb25SZXN1bHRIYW5kbGluZ1R5cGUAAAM0MQB4AAAABSRJRAAQAAAAAD2CT0qugalZp//nLI2NPU0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACkAAABDb252ZXJ0UmVzdENhbGxBY3Rpb25SZXF1ZXN0SGFuZGxpbmdUeXBlAAADNDIAbQAAAAUkSUQAEAAAAAD8u5jw3e74X6qFYBABB0ysAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAQ29udmVydFJlc3VsdEhhbmRsaW5nRGF0YVR5cGUAAAM0MwBqAAAABSRJRAAQAAAAAO7kuGIYo7NfnNM3ZsbM/kkCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABsAAABNZW51UGFnZVNldHRpbmdzQ29udmVyc2lvbgAAAzQ0AGcAAAAFJElEABAAAAAADrc+9SBZSV2hnzOzYrM9GQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGAAAAFJlYWRPbmx5U3R5bGVDb252ZXJzaW9uAAADNDUAbQAAAAUkSUQAEAAAAAD2tKQ036hcWpSX0FERzE4QAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAU2lkZWJhclRvZ2dsZUJ1dHRvbkNvbnZlcnNpb24AAAM0NgBqAAAABSRJRAAQAAAAAGiV3wRleD5dtGWNahKn+I4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABsAAABSZW1vdmVSZXBvcnRQYW5lQ29udmVyc2lvbgAAAzQ3AGwAAAAFJElEABAAAAAAo+rsz/0RTFSwC0NglHfbpQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHQAAAEh5YnJpZFBob25lUHJvZmlsZUNvbnZlcnNpb24AAAM0OABtAAAABSRJRAAQAAAAAGEUuc2Ki7pSp3jXTiJSG/YCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB4AAABDbGVhck9mZmxpbmVQcm9maWxlQ29udmVyc2lvbgAAAAA=" + } + ] +} diff --git a/lib/mxrb/templates/project/7.17.0.json b/lib/mxrb/templates/project/7.17.0.json new file mode 100644 index 0000000..766ac2d --- /dev/null +++ b/lib/mxrb/templates/project/7.17.0.json @@ -0,0 +1,50 @@ +{ + "format_version": "v1", + "units": [ + { + "unit_id": "4e70063d-53e1-477a-ab92-6b763330c293", + "container_id": "861db36b-5284-4f08-b244-e67901d99a5e", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Settings$ProjectSettings", + "contents": "LAoAAAUkSUQAEAAAAAA9BnBO4VN6R6uSa3YzMMKTAiRUeXBlABkAAABTZXR0aW5ncyRQcm9qZWN0U2V0dGluZ3MABFNldHRpbmdzAN8JAAAQMAACAAAAAzEA1wAAAAUkSUQAEAAAAAC82XyoYWY3WqFPOBeQlP/sAiRUeXBlAB8AAABGb3JtcyRXZWJVSVByb2plY3RTZXR0aW5nc1BhcnQACEVuYWJsZURvd25sb2FkUmVzb3VyY2VzAAAIRW5hYmxlTWljcm9mbG93UmVhY2hhYmlsaXR5QW5hbHlzaXMAAQhFbmFibGVXaWRnZXRCdW5kbGluZwAAAlRoZW1lAAoAAAAoRGVmYXVsdCkAAlRoZW1lTW9kdWxlTmFtZQANAAAAVUlfUmVzb3VyY2VzAAADMgBSAAAABSRJRAAQAAAAACCuBxzrK2RQpMT8mGbKGecCJFR5cGUAKAAAAFNldHRpbmdzJEludGVncmF0aW9uUHJvamVjdFNldHRpbmdzUGFydAAAAzMASQQAAAUkSUQAEAAAAADYBM3PLzOxXIxk0/GEpHcxAiRUeXBlAB8AAABTZXR0aW5ncyRDb25maWd1cmF0aW9uU2V0dGluZ3MABENvbmZpZ3VyYXRpb25zAPADAAAQMAADAAAAAzEA4QEAAAUkSUQAEAAAAAA/ify2Qaq2QaUHTXNqsCXGAiRUeXBlAB0AAABTZXR0aW5ncyRTZXJ2ZXJDb25maWd1cmF0aW9uAAJBcHBsaWNhdGlvblJvb3RVcmwAFwAAAGh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC8ABENvbnN0YW50VmFsdWVzAAwAAAAQMAADAAAAAARDdXN0b21TZXR0aW5ncwAMAAAAEDAAAwAAAAACRGF0YWJhc2VOYW1lAAgAAABkZWZhdWx0AAJEYXRhYmFzZVBhc3N3b3JkAAEAAAAAAkRhdGFiYXNlVHlwZQAHAAAASHNxbGRiAAJEYXRhYmFzZVVybAABAAAAAAhEYXRhYmFzZVVzZUludGVncmF0ZWRTZWN1cml0eQAAAkRhdGFiYXNlVXNlck5hbWUAAQAAAAAIRW11bGF0ZUNsb3VkU2VjdXJpdHkAAQJFeHRyYUp2bVBhcmFtZXRlcnMAAQAAAAAQSHR0cFBvcnROdW1iZXIAkB8AABBNYXhKYXZhSGVhcFNpemUAAAAAAAJOYW1lAAgAAABEZWZhdWx0AAhPcGVuQWRtaW5Qb3J0AAEIT3Blbkh0dHBQb3J0AAAQU2VydmVyUG9ydE51bWJlcgCaHwAAAAMyAP0BAAAFJElEABAAAAAA0srtw7F/vkKOAg36a+Au6gIkVHlwZQAdAAAAU2V0dGluZ3MkU2VydmVyQ29uZmlndXJhdGlvbgACQXBwbGljYXRpb25Sb290VXJsABcAAABodHRwOi8vbG9jYWxob3N0OjgwODAvAARDb25zdGFudFZhbHVlcwAMAAAAEDAAAwAAAAAEQ3VzdG9tU2V0dGluZ3MADAAAABAwAAMAAAAAAkRhdGFiYXNlTmFtZQAHAAAAbXhkZXY0AAJEYXRhYmFzZVBhc3N3b3JkAAcAAABtZW5kaXgAAkRhdGFiYXNlVHlwZQALAAAAUG9zdGdyZVNxbAACRGF0YWJhc2VVcmwADwAAAGxvY2FsaG9zdDo1NDMyAAhEYXRhYmFzZVVzZUludGVncmF0ZWRTZWN1cml0eQAAAkRhdGFiYXNlVXNlck5hbWUABwAAAG1lbmRpeAAIRW11bGF0ZUNsb3VkU2VjdXJpdHkAAQJFeHRyYUp2bVBhcmFtZXRlcnMAAQAAAAAQSHR0cFBvcnROdW1iZXIAkB8AABBNYXhKYXZhSGVhcFNpemUAAAAAAAJOYW1lAAcAAABwZy1kZXYACE9wZW5BZG1pblBvcnQAAQhPcGVuSHR0cFBvcnQAABBTZXJ2ZXJQb3J0TnVtYmVyAJofAAAAAAADNAD5AQAABSRJRAAQAAAAAGWBmiNWUjxTsFUwDkCaUAECJFR5cGUAFwAAAFNldHRpbmdzJE1vZGVsU2V0dGluZ3MAAkFmdGVyU3RhcnR1cE1pY3JvZmxvdwAnAAAAQXBwQ2xvdWRTZXJ2aWNlcy5TdGFydEFwcENsb3VkU2VydmljZXMACEFsbG93VXNlck11bHRpcGxlU2Vzc2lvbnMAAQJCZWZvcmVTaHV0ZG93bk1pY3JvZmxvdwABAAAAAAJEZWZhdWx0VGltZVpvbmVDb2RlAAEAAAAACEVuYWJsZURhdGFTdG9yYWdlTmV3UXVlcnlIYW5kbGluZwAACEVuYWJsZURhdGFTdG9yYWdlT3B0aW1pc3RpY0xvY2tpbmcAAQhFbmZvcmNlRGF0YVN0b3JhZ2VVbmlxdWVuZXNzAAECRmlyc3REYXlPZldlZWsACAAAAERlZmF1bHQAAkhhc2hBbGdvcml0aG0ABwAAAEJDcnlwdAACSGVhbHRoQ2hlY2tNaWNyb2Zsb3cAAQAAAAACUm91bmRpbmdNb2RlAAcAAABIYWxmVXAAAlNjaGVkdWxlZEV2ZW50VGltZVpvbmVDb2RlAAgAAABFdGMvVVRDAAhVc2VEZXByZWNhdGVkQ2xpZW50Rm9yV2ViU2VydmljZUNhbGxzAAEAAzUAZAAAAAUkSUQAEAAAAAB3DiMYPnXkW5+aPoIYJXkpAiRUeXBlABwAAABTZXR0aW5ncyRDb252ZW50aW9uU2V0dGluZ3MACExvd2VyQ2FzZU1pY3JvZmxvd1ZhcmlhYmxlcwAAAAM2ACMBAAAFJElEABAAAAAAw7MqoNN5i1usSTqPimiKUAIkVHlwZQAaAAAAU2V0dGluZ3MkTGFuZ3VhZ2VTZXR0aW5ncwACRGVmYXVsdExhbmd1YWdlQ29kZQAGAAAAZW5fVVMABExhbmd1YWdlcwC1AAAAEDAAAwAAAAMxAKYAAAAFJElEABAAAAAA5tlnfRFBJUKOSEydeIyLagIkVHlwZQAPAAAAVGV4dHMkTGFuZ3VhZ2UACENoZWNrQ29tcGxldGVuZXNzAAACQ29kZQAGAAAAZW5fVVMAAkN1c3RvbURhdGVGb3JtYXQAAQAAAAACQ3VzdG9tRGF0ZVRpbWVGb3JtYXQAAQAAAAACQ3VzdG9tVGltZUZvcm1hdAABAAAAAAAAAAM3AGEAAAAFJElEABAAAAAAhFK0TVmeBVWVVGxpzH9hjgIkVHlwZQAdAAAAU2V0dGluZ3MkQ2VydGlmaWNhdGVTZXR0aW5ncwAEQ2VydGlmaWNhdGVzAAwAAAAQMAADAAAAAAADOABoAAAABSRJRAAQAAAAAPVpIJxXHdBct0qEqojM28gCJFR5cGUAHQAAAFNldHRpbmdzJEphdmFBY3Rpb25zU2V0dGluZ3MACEdlbmVyYXRlUG9zdGZpeGVzRm9yUGFyYW1ldGVycwABAAAA" + }, + { + "unit_id": "b62986ab-1ec9-5c84-9d33-1f0829ffce9b", + "container_id": "861db36b-5284-4f08-b244-e67901d99a5e", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Navigation$NavigationDocument", + "contents": "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" + }, + { + "unit_id": "57d2ecb5-6c2f-4ffa-a0b3-a37a40da3adf", + "container_id": "861db36b-5284-4f08-b244-e67901d99a5e", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Texts$SystemTextCollection", + "contents": "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YW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ADAAAABEZSBvcGdlZ2V2ZW4gd2FhcmRlIG1hZyBhbGxlZW4gY2lqZmVycyBiZXZhdHRlbgAAAAAAAzUA5QEAAAUkSUQAEAAAAACdGcoU7afbQIWCPv52la0PAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAgAAAAbWVuZGl4LnBsYXRmb3JtLmludmFsaWRfaW50ZWdlcgADVGV4dABzAQAABSRJRAAQAAAAAM1vpacrxT1Iqus8qX4qmV8CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zADcBAAAQMAADAAAAAzEAjgAAAAUkSUQAEAAAAADMSgoHlvnMU7xMbI2/hyq4AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAwAAAAVGhpcyB2YWx1ZSBzaG91bGQgYmUgYSBudW1iZXIgd2l0aG91dCBmcmFjdGlvbnMAAAMyAJcAAAAFJElEABAAAAAAz5sH6HBTn1KlkUlOGRPmIgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAOQAAAERlIG9wZ2VnZXZlbiB3YWFyZGUgbW9ldCBlZW4gZ2V0YWwgem9uZGVyIGRlY2ltYWxlbiB6aWpuAAAAAAADNgDgAQAABSRJRAAQAAAAAMH+oOVbTU5NkrWW8kiqV9MCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACIAAABtZW5kaXgucGxhdGZvcm0uaW50ZWdlcl9hYm92ZV9tYXgAA1RleHQAbAEAAAUkSUQAEAAAAACue9rbv+UgSoG1FNNG6AlAAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAwAQAAEDAAAwAAAAMxAIsAAAAFJElEABAAAAAAYZqsuqOTJ12hLsPA8/p3cgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALQAAAFRoaXMgdmFsdWUgc2hvdWxkIGJlIHNtYWxsZXIgdGhhbiAyMTQ3NDgzNjQ4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YW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADAAAABQYXNzd29yZCBzaG91bGQgY29udGFpbiBhbiB1cHBlcmNhc2UgY2hhcmFjdGVyLgAAAzIAjAAAAAUkSUQAEAAAAACc+kcKdR+OV62Mg6XlzWBfAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAuAAAASGV0IHdhY2h0d29vcmQgbW9ldCBlZW4gaG9vZmRsZXR0ZXIgYmV2YXR0ZW4uAAAAAAADMzcA3AEAAAUkSUQAEAAAAABn8UoIllUcWID9S1EaSwXPAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAhAAAAbWVuZGl4LnBsYXRmb3JtLm1pc3NpbmdMb3dlcmNhc2UAA1RleHQAaQEAAAUkSUQAEAAAAAB0gLDCTtdNVr+NTjOLrEZtAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAtAQAAEDAAAwAAAAMxAI0AAAAFJElEABAAAAAAGu6SRDtlvl+q+lRwwLPbjQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALwAAAFBhc3N3b3JkIHNob3VsZCBjb250YWluIGEgbG93ZXJjYXNlIGNoYXJhY3Rlci4AAAMyAI4AAAAFJElEABAAAAAAN5JEV7am0lSrFM6SYsHrgQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAMAAAAEhldCB3YWNodHdvb3JkIG1vZXQgZWVuIGtsZWluZSBsZXR0ZXIgYmV2YXR0ZW4uAAAAAAADMzgAGwIAAAUkSUQAEAAAAACdzz87MeOCVrOXnb9hiTxdAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAWAAAAbWVuZGl4LnNpZ25pbi5odHRwNDE5AANUZXh0ALMBAAAFJElEABAAAAAAwyvu3nU3NlO6iOuUBobGswIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAdwEAABAwAAMAAAADMQC2AAAABSRJRAAQAAAA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dWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AGIAAABEZWNpbWFsZSBnZXRhbGxlbiBtb2V0ZW4gbWluZGVyIGdldGFsbGVuIGJldmF0dGVuIGRhbiAyMCBkZWNpbWFsZW4gdm9vciBlbiA4IGdldGFsbGVuIG5hIGRlIHB1bnQuAAAAAAADNDIAAgIAAAUkSUQAEAAAAABoTwJgJAj/X6VkRi9znIoYAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAkAAAAbWVuZGl4LmxpYi5WYWxpZGF0aW9ucy5yYW5nZV9udW1iZXIAA1RleHQAjAEAAAUkSUQAEAAAAAAQXaQqBvZHULLHkEJPGNgjAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwBQAQAAEDAAAwAAAAMxAJsAAAAFJElEABAAAAAAZTE/rHak3VeOW3xZ+ySWDAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAPQAAAFRoaXMgbnVtYmVyIGlzIHRvbyBiaWcgYW5kIGNhbm5vdCBiZSBhY2N1cmF0ZWx5IHJlcHJlc2VudGVkLgAAAzIAowAAAAUkSUQAEAAAAAClQFmaAPVLW6D2bc1jdB5TAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABFAAAARGl0IGdldGFsIGlzIHRlIGdyb290IGVuIGthbiBuaWV0IG5hdXdrZXVyaWcgd29yZGVuIGdlcmVwcmVzZW50ZWVyZC4AAAAAAAM0MwCwAQAABSRJRAAQAAAAANX5BPQzMSVfstqFTMcylsgCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACUAAABtZW5kaXgubGliLlZhbGlkYXRpb25zLnJhbmdlX2ludGVnZXIAA1RleHQAOQEAAAUkSUQAEAAAAAAxGPZ2Tk5EVZ624fVmvjWbAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwD9AAAAEDAAAwAAAAMxAHYAAAAFJElEABAAAAAABnDXcj08W1y/LYSR7xjqLAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRp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0wIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJgAAAG14dWkuc3lzLlVJLm5vbl9jb25uZWN0aW9uX3N5bmNfZXJyb3IAA1RleHQA9wEAAAUkSUQAEAAAAAAV8V4YyJ4iUrwys/M7jZXjAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwC7AQAAEDAAAwAAAAMxAN8AAAAFJElEABAAAAAAmZGmC+KXHl6JlNEXAdNYNgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAgQAAAEhldCBzeW5jaHJvbmlzZXJlbiB2YW4gdXcgd2lqemlnaW5nZW4gaXMgbWlzbHVrdC4gT20gdSBpbiBzdGFhdCB0ZSBzdGVsbGVuIG9tIGRvb3IgdGUgZ2FhbiB6aWpuIGRlIHdpanppZ2luZ2VuIG9uZ2VkYWFuIGdlbWFha3QuAAADMgDKAAAABSRJRAAQAAAAAIcZL8cH0DpZu93F1cpFRDUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AGwAAABUaGUgc3luY2hyb25pemF0aW9uIGhhcyBmYWlsZWQgZHVlIHRvIGFuIGVycm9yLiBUbyBhbGxvdyB5b3UgdG8gY29udGludWUsIHlvdXIgY2hhbmdlcyBoYXZlIGJlZW4gZGlzY2FyZGVkLgAAAAAAAzc5AMYBAAAFJElEABAAAAAAROiSuBvMN1aNs0+rfg0STgIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAGwAAAG14dWkuc3lzLlVJLmNvbmZpcm1fZGVsZXRlAANUZXh0AFkBAAAFJElEABAAAAAAQacRYWg7TVSXqstnATlX0QIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAHQEAABAwAAMAAAADMQCHAAAABSRJRAAQAAAAACmY1Vtyi4dQtmiaiGPdy/oCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ACkAAABXZWV0IHUgemVrZXIgZGF0IHUgZGl0IHdpbHQgdmVyd2lqZGVyZW4/AAADMgCEAAAABSRJRAAQAAAAAEKfrji54V5Qj72ibFwcHl4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ACYAAABBcmUgeW91IHN1cmUgeW91IHdhbnQgdG8gZGVsZXRlIHRoaXM/AAAAAAAAAA==" + }, + { + "unit_id": "bd6f65bd-2393-539d-9388-4684a54d0646", + "container_id": "861db36b-5284-4f08-b244-e67901d99a5e", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Security$ProjectSecurity", + "contents": "VAQAAAUkSUQAEAAAAAC9ZW+9kyOdU5OIRoSlTQZGAiRUeXBlABkAAABTZWN1cml0eSRQcm9qZWN0U2VjdXJpdHkAAkFkbWluUGFzc3dvcmQAAgAAADEAAkFkbWluVXNlck5hbWUACAAAAE14QWRtaW4AAkFkbWluVXNlclJvbGUAAQAAAAAIQ2hlY2tTZWN1cml0eQABBERlbW9Vc2VycwCSAQAAEDAAAgAAAAMxAMkAAAAFJElEABAAAAAAD8QgBhnLAECUGViBCTkP7gIkVHlwZQAWAAAAU2VjdXJpdHkkRGVtb1VzZXJJbXBsAAJFbnRpdHkAFwAAAEFkbWluaXN0cmF0aW9uLkFjY291bnQAAlBhc3N3b3JkAAsAAAB0b3FFWVNtRzY4AAJVc2VyTmFtZQATAAAAZGVtb19hZG1pbmlzdHJhdG9yAARVc2VyUm9sZXMAIQAAABAwAAEAAAACMQAOAAAAQWRtaW5pc3RyYXRvcgAAAAMyALcAAAAFJElEABAAAAAAZT44j85+VEeIrehkQObKCgIkVHlwZQAWAAAAU2VjdXJpdHkkRGVtb1VzZXJJbXBsAAJFbnRpdHkAFwAAAEFkbWluaXN0cmF0aW9uLkFjY291bnQAAlBhc3N3b3JkAAsAAABGRXZqWHltcDU0AAJVc2VyTmFtZQAKAAAAZGVtb191c2VyAARVc2VyUm9sZXMAGAAAABAwAAEAAAACMQAFAAAAVXNlcgAAAAAIRW5hYmxlRGVtb1VzZXJzAAEIRW5hYmxlR3Vlc3RBY2Nlc3MAAANGaWxlRG9jdW1lbnRBY2Nlc3MAbAAAAAUkSUQAEAAAAAAIduuNSeGKRp/4JDU20NuvAiRUeXBlACkAAABTZWN1cml0eSRGaWxlRG9jdW1lbnRBY2Nlc3NSdWxlQ29udGFpbmVyAARBY2Nlc3NSdWxlcwAMAAAAEDAAAwAAAAAAAkd1ZXN0VXNlclJvbGUAAQAAAAADSW1hZ2VBY2Nlc3MAZQAAAAUkSUQAEAAAAABRZ3yQBxdRRKLK3P7Cb5Y+AiRUeXBlACIAAABTZWN1cml0eSRJbWFnZUFjY2Vzc1J1bGVDb250YWluZXIABEFjY2Vzc1J1bGVzAAwAAAAQMAADAAAAAAADUGFzc3dvcmRQb2xpY3lTZXR0aW5ncwCPAAAABSRJRAAQAAAAAP19SHgAaVBFuvXItKSIgrgCJFR5cGUAIAAAAFNlY3VyaXR5JFBhc3N3b3JkUG9saWN5U2V0dGluZ3MAEE1pbmltdW1MZW5ndGgABgAAAAhSZXF1aXJlRGlnaXQAAQhSZXF1aXJlTWl4ZWRDYXNlAAEIUmVxdWlyZVN5bWJvbAAAAAJTZWN1cml0eUxldmVsAA0AAABDaGVja05vdGhpbmcAAlNpZ25Jbk1pY3JvZmxvdwABAAAAAARVc2VyUm9sZXMADAAAABAwAAIAAAAAAA==" + }, + { + "unit_id": "1fcc2823-c303-5a27-b6c7-78b04c96c88f", + "container_id": "861db36b-5284-4f08-b244-e67901d99a5e", + "module": null, + "containment": "ProjectConversion", + "name": "", + "type": "Projects$ProjectConversion", + "contents": "JSoAAAUkSUQAEAAAAAAjKMwfA8MnWrbHeLBMlsiPAiRUeXBlABsAAABQcm9qZWN0cyRQcm9qZWN0Q29udmVyc2lvbgAET25lVGltZUNvbnZlcnNpb25zAMwpAAAQMAADAAAAAzEAagAAAAUkSUQAEAAAAABgh+VGzLKwX4DHvEWFciIEAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAbAAAARGF0YVR5cGVUb014VHlwZUNvbnZlcnNpb24AAAMyAHQAAAAFJElEABAAAAAAXXWCgmqgaFKxJ7O8eLTowAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAFBhcmFtZXRlck14VHlwZVRvQmFzaWNUeXBlQ29udmVyc2lvbgAAAzMAfgAAAAUkSUQAEAAAAABKZ01/le4vXbilsr77D8nUAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAvAAAAUGFyYW1ldGVyTWFwcGluZ0FyZ3VtZW50VG9CYXNpY1ZhbHVlQ29udmVyc2lvbgAAAzQAfgAAAAUkSUQAEAAAAABALfUN6LGFUKNUAXo27hC6AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAvAAAAQ29udmVydEluaGVyaXRhbmNlUGFyYW1ldGVyTmFtZUluRXhwb3J0TWFwcGluZwAAAzUAeQAAAAUkSUQAEAAAAACU6DgsBwwwXaQNvGUVFAczAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAqAAAAQ29udmVydEluaGVyaXRhbmNlQ29tcGxleFBhcmFtZXRlck1hcHBpbmcAAAM2AGYAAAAFJElEABAAAAAAO8d6MStm01uLEqtgCb1SYwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFwAAAENvbnZlcnRJbmhlcml0YW5jZVBhdGgAAAM3AHsAAAAFJElEABAAAAAAwVpEHNS9NVSwGrgVdohsOQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALAAAAENvbnZlcnRFeHBvcnRNYXBwaW5nRm9yQ2hvaWNlT2JqZWN0SGFuZGxpbmcAAAM4AG4AAAAFJElEABAAAAAAUe6JZK21dFirTJkoM9E2wwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAENvbnZlcnRDaG9pY2VFbGVtZW50T2NjdXJyZW5jZQAAAzkAcwAAAAUkSUQAEAAAAADLlUG+fnuCUbNBZ1f1FtP/AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAAQ29udmVydEluaGVyaXRhbmNlRWxlbWVudE9jY3VycmVuY2UAAAMxMABnAAAABSRJRAAQAAAAAG1JEB62/KRdtvhVckvFrY8CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABgAAABDb252ZXJ0WG1sUHJpbWl0aXZlVHlwZQAAAzExAHQAAAAFJElEABAAAAAA/hNzuOq8oVKe17JU3OWalQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAENvbnZlcnRYbWxQcmltaXRpdmVUeXBlSW50ZWdlclRvTG9uZwAAAzEyAHQAAAAFJElEABAAAAAAXhCypAZZqFufplg8CEoyigIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAENvbnZlcnRDaG9pY2VJbmhlcml0YW5jZUFzc29jaWF0aW9ucwAAAzEzAGQAAAAFJElEABAAAAAA3I701Oc4c1G1kK4h9ShxuwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFQAAAENvbnZlcnRJc0RlZmF1bHRUeXBlAAADMTQAdAAAAAUkSUQAEAAAAACiZuF7YbMwV5NFr5RvMbzqAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAlAAAAQ29udmVydEV4cGVjdGVkQ29udGVudFR5cGVzVG9Vc2VNdG9tAAADMTUAZgAAAAUkSUQAEAAAAAAS8E2X6gJTXLF6yOP+/rzQAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAXAAAAQ29udmVydFhtbEV4cG9zZWROYW1lcwAAAzE2AGsAAAAFJElEABAAAAAAzMzyowVQtVGP2h9Krv/f2QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAENvbnZlcnRKc29uRWxlbWVudHNPcHRpb25hbAAAAzE3AGsAAAAFJElEABAAAAAAoG3S4HucNFmBkfkmdgNkTgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAENvbnZlcnRKc29uRWxlbWVudHNOaWxsYWJsZQAAAzE4AGYAAAAFJElEABAAAAAAR7wixa+H7Vq/liD5nidegAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFwAAAENvbnZlcnROdWxsVmFsdWVPcHRpb24AAAMxOQBsAAAABSRJRAAQAAAAAJ+x4zWNxPNRm8XE6UaZ3GUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB0AAABDb252ZXJ0U3lzdGVtSWREYXRhQXR0cmlidXRlAAADMjAAcgAAAAUkSUQAEAAAAAD3qJuhKW6NX5FrRLQLP/ojAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAjAAAAQ2xlYXJJbnZpc2libGVTZWFyY2hCYXJzQ29udmVyc2lvbgAAAzIxAGsAAAAFJElEABAAAAAAvyXQLKhn8VWbsU38EVVu7gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAE5ld0J1dHRvbkVudGl0eVRvRW50aXR5UGF0aAAAAzIyAHUAAAAFJElEABAAAAAAdpWXrFfGp1+VhgOcC3guNAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJgAAAFNldERhdGFWaWV3Q29udHJvbEJhclRvTnVsbENvbnZlcnNpb24AAAMyMwB0AAAABSRJRAAQAAAAAK/PiY3C+XJZjDTzqx4Jm3ACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACUAAABEYXRhVmlld0NvbnRyb2xCYXJUb0Zvb3RlckNvbnZlcnNpb24AAAMyNABtAAAABSRJRAAQAAAAABTelvwqGrhWnzCXYGrz9b8CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB4AAABMYXlvdXRDYWxsT3B0aW9uYWxDb252ZXJzaW9uIAAAAzI1AHcAAAAFJElEABAAAAAARqxZR/9BqlShICYfhTr2lQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKAAAAFBsYWNlaG9sZGVySWRlbnRpZmllcnNUb05hbWVzQ29udmVyc2lvbgAAAzI2AHYAAAAFJElEABAAAAAA115YfHtdwFqVAhrABffugQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAExheW91dENhbGxBcmd1bWVudElkZW50aWZpZXJDb252ZXJzaW9uAAADMjcAdQAAAAUkSUQAEAAAAAApoQKXwFKOVa2EIQ7z7uVHAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAUmVtb3ZlRHVwbGljYXRlVHJhbnNsYXRpb25zQ29udmVyc2lvbgAAAzI4AGwAAAAFJElEABAAAAAAhv6T0OWCG1CA7yQus9LObwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHQAAAEphdmFBY3Rpb25TZXR0aW5nc0NvbnZlcnNpb24AAAMyOQB3AAAABSRJRAAQAAAAAJom5Fei2HNbo1oqaktLATUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACgAAABDb252ZXJ0UmVzdENhbGxBY3Rpb25SZXN1bHRIYW5kbGluZ1R5cGUAAAMzMAB4AAAABSRJRAAQAAAAAD2CT0qugalZp//nLI2NPU0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACkAAABDb252ZXJ0UmVzdENhbGxBY3Rpb25SZXF1ZXN0SGFuZGxpbmdUeXBlAAADMzEAbQAAAAUkSUQAEAAAAAD8u5jw3e74X6qFYBABB0ysAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAQ29udmVydFJlc3VsdEhhbmRsaW5nRGF0YVR5cGUAAAMzMgBqAAAABSRJRAAQAAAAAO7kuGIYo7NfnNM3ZsbM/kkCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABsAAABNZW51UGFnZVNldHRpbmdzQ29udmVyc2lvbgAAAzMzAGcAAAAFJElEABAAAAAADrc+9SBZSV2hnzOzYrM9GQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGAAAAFJlYWRPbmx5U3R5bGVDb252ZXJzaW9uAAADMzQAbQAAAAUkSUQAEAAAAAD2tKQ036hcWpSX0FERzE4QAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAU2lkZWJhclRvZ2dsZUJ1dHRvbkNvbnZlcnNpb24AAAMzNQBqAAAABSRJRAAQAAAAAGiV3wRleD5dtGWNahKn+I4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABsAAABSZW1vdmVSZXBvcnRQYW5lQ29udmVyc2lvbgAAAzM2AG8AAAAFJElEABAAAAAAEFk/Q5r6LVmc8TsMwP814gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIAAAAERhdGFzdG9yYWdlVW5pcXVlbmVzc0NvbnZlcnNpb24AAAMzNwBpAAAABSRJRAAQAAAAAOilZvys+IpSloECcPS3RU4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABoAAABDb252ZXJ0V2Vic2VydmljZU5vUmVzdWx0AAADMzgAZAAAAAUkSUQAEAAAAAAaTuB9pBWFU56qQbmZXKI0AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAVAAAAU3RhdGlzdGljc0NvbnZlcnNpb24AAAMzOQBrAAAABSRJRAAQAAAAACLz1hiGABpSnZmzbyTuQN0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABPbmVUb3BsZXZlbEJ1dHRvbkNvbnZlcnNpb24AAAM0MABsAAAABSRJRAAQAAAAAKPq7M/9EUxUsAtDYJR326UCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB0AAABIeWJyaWRQaG9uZVByb2ZpbGVDb252ZXJzaW9uAAADNDEAbQAAAAUkSUQAEAAAAAAzEV7XYyRhVL9a0J3kc120AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAT2xkSHlicmlkUGhvbmVUb05ld0NvbnZlcnNpb24AAAM0MgBmAAAABSRJRAAQAAAAANX8yIMKEoxWraSTLK2v8hYCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABcAAABMb2dvdXRCdXR0b25Db252ZXJzaW9uAAADNDMAbAAAAAUkSUQAEAAAAACX4bonXcdTWZg8+A3Z+onxAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAdAAAATW92ZVByb2ZpbGVzVG9MaXN0Q29udmVyc2lvbgAAAzQ0AG4AAAAFJElEABAAAAAAsqNxb4pqUVSctS6Iq2cEqAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAFJlbW92ZVVudXNlZFByb2ZpbGVzQ29udmVyc2lvbgAAAzQ1AHMAAAAFJElEABAAAAAAQVg7+4YqUlCKA6p0Vt7boQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJAAAAFNldFRhYmxldEFuZFBob25lSG9tZVBhZ2VDb252ZXJzaW9uAAADNDYAegAAAAUkSUQAEAAAAAD3dV5f1VdTW5tqgeCTaOWgAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQArAAAARml4SURzT2ZEcm9wRG93bkVtcHR5T3B0aW9uVGV4dHNDb252ZXJzaW9uAAADNDcAZgAAAAUkSUQAEAAAAAAWpmr1+5I6VJHq8qOrz1hBAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAXAAAARmlsZURvY3VtZW50Q29udmVyc2lvbgAAAzQ4AHYAAAAFJElEABAAAAAA8CVX97GAyFO3srAlfWSHtgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAERhdGFTdG9yYWdlT3B0aW1pc3RpY0xvY2tpbmdDb252ZXJzaW9uAAADNDkAdQAAAAUkSUQAEAAAAABlY8lbqn+LWKQe/2eZfEIaAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAQ29udmVydFdzZGxWYWxpZGF0ZVNjaGVtYU5vdFN1cHBvcnRlZAAAAzUwAHIAAAAFJElEABAAAAAAV3Hr2+nrpVqwjU2cJf6CgwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIwAAAENvbnZlcnRSb290RWxlbWVudFBhcmFtZXRlck1hcHBpbmcAAAM1MQBxAAAABSRJRAAQAAAAAI6b4CpjhUpSlR/YVEgzzE4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACIAAABDb252ZXJ0RXhwb3J0QWN0aW9uUmVzdWx0SGFuZGxpbmcAAAM1MgBoAAAABSRJRAAQAAAAAB5QI49DglxVvCrhAQZEs5ECJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABkAAABDb252ZXJ0QWN0aW9uQ29udGVudFR5cGUAAAM1MwB0AAAABSRJRAAQAAAAAEak1dCxa9hYtULXMX/zvRsCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACUAAABDb252ZXJ0TWFwcGluZ0VsZW1lbnRQYXRoVG9YbWxPckpzb24AAAM1NABkAAAABSRJRAAQAAAAAM1G1o1dJ9FXqUEua7C943sCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABUAAABMaW5rQnV0dG9uQ29udmVyc2lvbgAAAzU1AGsAAAAFJElEABAAAAAACDOgmh5+B12NFGS4ll7wSAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAEV4dGVuZFByb2ZpbGVLaW5kQ29udmVyc2lvbgAAAzU2AGwAAAAFJElEABAAAAAA05AD/nvzlVeBl/ZNGsHT1AIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHQAAAFJlcXVpcmVkVmFsaWRhdGlvbkNvbnZlcnNpb24AAAM1NwBtAAAABSRJRAAQAAAAAIsjKrS2hFNYugLjnApBACgCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB4AAABPcHRpb25hbFJldHVyblZhbHVlQ29udmVyc2lvbgAAAzU4AGoAAAAFJElEABAAAAAAa+gE8TMrZFua8s4jDMXArAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGwAAAERlbW9Vc2VyUGFzc3dvcmRDb252ZXJzaW9uAAADNTkAcAAAAAUkSUQAEAAAAAAeTSBnA7TAULUWNkvSZzC+AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAhAAAARml4SURzT2ZOZXdFeHByZXNzaW9uc0NvbnZlcnNpb24AAAM2MAByAAAABSRJRAAQAAAAAI1eC9ZBt6VZjau9sCoaxZcCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACMAAABDb25zdGFudHNEYXRhVHlwZVN0b3JhZ2VDb252ZXJzaW9uAAADNjEAcwAAAAUkSUQAEAAAAADUf06P9xZiWJtyagk/vRZ7AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAATWljcm9mbG93c0RhdGFUeXBlU3RvcmFnZUNvbnZlcnNpb24AAAM2MgB1AAAABSRJRAAQAAAAAMynBD/fJdxbr4x3JjJtGY0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACYAAABNaWNyb2Zsb3dzRXJyb3JIYW5kbGluZ0Zsb3dDb252ZXJzaW9uAAADNjMAdAAAAAUkSUQAEAAAAABrYWL2yXPHV4XQyf+mE4xHAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAlAAAARGF0YVN0b3JhZ2VOZXdRdWVyeUhhbmRsZXJDb252ZXJzaW9uAAADNjQAbwAAAAUkSUQAEAAAAAAjoDb9tgrZWr4OLG7F6shGAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAgAAAATWljcm9mbG93c1BhdGhzVG9SZWZzQ29udmVyc2lvbgAAAzY1AHYAAAAFJElEABAAAAAAoFfj99b3OVShqIbDegfaVAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAERvY3VtZW50VGVtcGxhdGVzUGF0aHNUb1JlZnNDb252ZXJzaW9uAAADNjYAbwAAAAUkSUQAEAAAAABcaq9HsRSXWbiQGyyBUBHPAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAgAAAAQ29udmVydEV4cG9zZWRFbnRpdHlFeHBvc2VkTmFtZQAAAzY3AHIAAAAFJElEABAAAAAAqbZ2ZRomQFWeTAsEds3HXwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIwAAAENvbnZlcnREYXRhVHlwZXNUb05ld1N0b3JhZ2VGb3JtYXQAAAM2OABzAAAABSRJRAAQAAAAAHrntaz0GKZfmNtJx2U6/4sCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACQAAABDb252ZXJ0Q2xlYXJSZXN0Qm9keUZvckdldEFuZERlbGV0ZQAAAzY5AGgAAAAFJElEABAAAAAAPRODhGLuq16PvEeTQHhUGgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGQAAAENvbnZlcnRQYXRoVG9TZXJ2aWNlTmFtZQAAAzcwAGMAAAAFJElEABAAAAAAAsZCHnnq+VSSuSxYgciWowIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFAAAAENvbnZlcnRPcHRpbWl6ZWRYbWwAAAM3MQB2AAAABSRJRAAQAAAAAKqqeUwCbIBSkc4nNQbzclUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACcAAABDb252ZXJ0UHVibGlzaGVkUmVzdEF1dGhlbnRpY2F0aW9uVHlwZQAAAzcyAGsAAAAFJElEABAAAAAAI6+hlosmIFWX1LLAzb8HawIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAExheW91dFBsYWNlaG9sZGVyQ29udmVyc2lvbgAAAzczAGUAAAAFJElEABAAAAAA6Y2O/J4YR1m2N/mSf1S51gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFgAAAFRoZW1lTW9kdWxlQ29udmVyc2lvbgAAAzc0AHAAAAAFJElEABAAAAAA3LRcyFAEGVCiKGqpj1T4TwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIQAAAEZpeEJ1aWxkaW5nQmxvY2tJbWFnZXNDb252ZXJzaW9uAAADNzUAagAAAAUkSUQAEAAAAADexdFI2IBgUb0/O6pNS9xMAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAbAAAAUGFnZXNQYXRoc1RvUmVmc0NvbnZlcnNpb24AAAM3NgB0AAAABSRJRAAQAAAAAJcZV8neJsZesCXCuQkKC+ACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACUAAABTZWFyY2hCdXR0b25Ub05vcm1hbEJ1dHRvbkNvbnZlcnNpb24AAAM3NwBnAAAABSRJRAAQAAAAADRkEeyrSeJco/Nm6HJmf5cCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABgAAABFdmVudFRvQWN0aW9uQ29udmVyc2lvbgAAAzc4AHEAAAAFJElEABAAAAAAdr34scA+5FqiztRC00QozgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIgAAAERhdGFTZXRzRGF0YVR5cGVTdG9yYWdlQ29udmVyc2lvbgAAAzc5AF8AAAAFJElEABAAAAAAufmuP0Oa11q6fAEs/Gis6QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAEAAAAENvbnZlcnRUaW1lT3V0cwAAAzgwAHwAAAAFJElEABAAAAAAdovXww3n4lymU1kKBE7shwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALQAAAENvbnZlcnRVc2VEZXByZWNhdGVkQ2xpZW50Rm9yV2ViU2VydmljZUNhbGxzAAADODEAZAAAAAUkSUQAEAAAAAASrIjsQVzZV7A+Sia84RcPAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAVAAAAU3lzdGVtVGV4dENvbnZlcnNpb24AAAM4MgB4AAAABSRJRAAQAAAAAFOWS4WRl0dcudDlVuH/x0wCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACkAAABQcmVkZWZpbmVkVmFsaWRhdGlvbkV4cHJlc3Npb25Db252ZXJzaW9uAAADODMAdgAAAAUkSUQAEAAAAADShyOazPk/XqMwNjK4oxrXAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAnAAAAVGV4dFRlbXBsYXRlRGVjaW1hbFByZWNpc2lvbkNvbnZlcnNpb24AAAM4NABvAAAABSRJRAAQAAAAAIUgwxu4F9Nfrx58qCbFsQ4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACAAAABFbGltaW5hdGVWZXJ0aWNhbEZsb3dDb252ZXJzaW9uAAADODUAdQAAAAUkSUQAEAAAAAAPM3Czs5gyU6RZVaKLMR33AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAQ29udmVydEltcG9ydE1hcHBpbmdQYXJhbWV0ZXJEYXRhVHlwZQAAAzg2AG4AAAAFJElEABAAAAAADpR8ZURMylOrzBTI2mx96AIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAENvbnZlcnRSZXN0T3BlcmF0aW9uUGFyYW1ldGVycwAAAzg3AHYAAAAFJElEABAAAAAAYObsW3GSgF2dJ9Jmmqi/RgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAENvbnZlcnRSZXN0T3BlcmF0aW9uUGFyYW1ldGVyTWljcm9mbG93AAADODgAegAAAAUkSUQAEAAAAAAB82XtVLNUVLazs2rvONm2AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQArAAAAVGVtcGxhdGVGb3JtQmFzZURvY3VtZW50YXRpb25VcmxDb252ZXJzaW9uAAADODkAagAAAAUkSUQAEAAAAABqSl1VwNDmW7EGKzzSdYqxAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAbAAAAR3JpZERlbGV0ZUJ1dHRvbkNvbnZlcnNpb24AAAM5MABtAAAABSRJRAAQAAAAAFlUXcMzkmpTqws1gs0y0k0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB4AAABTZXRDbG9zZVBhZ2VPblBvcHVwQ29udmVyc2lvbgAAAzkxAGMAAAAFJElEABAAAAAA5b+9xwSskl+zXCQ4SP2K4gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFAAAAE5ld0J1dHRvbkNvbnZlcnNpb24AAAM5MgB4AAAABSRJRAAQAAAAAKM6AkkmpSVWgmszXfJATVQCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACkAAABFbWFpbFZhbGlkYXRpb25DYXNlU2Vuc2l0aXZpdHlDb252ZXJzaW9uAAADOTMAaAAAAAUkSUQAEAAAAABDgp2bci1gW4ibCB2rn7C5AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAZAAAAR3JpZEVkaXRCdXR0b25Db252ZXJzaW9uAAAAAA==" + } + ] +} diff --git a/lib/mxrb/templates/project/7.5.0.json b/lib/mxrb/templates/project/7.5.0.json new file mode 100644 index 0000000..001bfc2 --- /dev/null +++ b/lib/mxrb/templates/project/7.5.0.json @@ -0,0 +1,50 @@ +{ + "format_version": "v1", + "units": [ + { + "unit_id": "4e70063d-53e1-477a-ab92-6b763330c293", + "container_id": "5714121f-eb32-470e-ac23-9dedf56439b6", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Settings$ProjectSettings", + "contents": "vgcAAAUkSUQAEAAAAAA9BnBO4VN6R6uSa3YzMMKTAiRUeXBlABkAAABTZXR0aW5ncyRQcm9qZWN0U2V0dGluZ3MABFNldHRpbmdzAHEHAAAQMAACAAAAAzEAtQAAAAUkSUQAEAAAAAC82XyoYWY3WqFPOBeQlP/sAiRUeXBlAB8AAABGb3JtcyRXZWJVSVByb2plY3RTZXR0aW5nc1BhcnQACEVuYWJsZURvd25sb2FkUmVzb3VyY2VzAAAIRW5hYmxlTWljcm9mbG93UmVhY2hhYmlsaXR5QW5hbHlzaXMAAQhFbmFibGVXaWRnZXRCdW5kbGluZwAAAlRoZW1lAAoAAAAoRGVmYXVsdCkAAAMyAFIAAAAFJElEABAAAAAAIK4HHOsrZFCkxPyYZsoZ5wIkVHlwZQAoAAAAU2V0dGluZ3MkSW50ZWdyYXRpb25Qcm9qZWN0U2V0dGluZ3NQYXJ0AAADMwBJAgAABSRJRAAQAAAAANgEzc8vM7FcjGTT8YSkdzECJFR5cGUAHwAAAFNldHRpbmdzJENvbmZpZ3VyYXRpb25TZXR0aW5ncwAEQ29uZmlndXJhdGlvbnMA8AEAABAwAAMAAAADMQDhAQAABSRJRAAQAAAAAD+J/LZBqrZBpQdNc2qwJcYCJFR5cGUAHQAAAFNldHRpbmdzJFNlcnZlckNvbmZpZ3VyYXRpb24AAkFwcGxpY2F0aW9uUm9vdFVybAAXAAAAaHR0cDovL2xvY2FsaG9zdDo4MDgwLwAEQ29uc3RhbnRWYWx1ZXMADAAAABAwAAMAAAAABEN1c3RvbVNldHRpbmdzAAwAAAAQMAADAAAAAAJEYXRhYmFzZU5hbWUACAAAAGRlZmF1bHQAAkRhdGFiYXNlUGFzc3dvcmQAAQAAAAACRGF0YWJhc2VUeXBlAAcAAABIc3FsZGIAAkRhdGFiYXNlVXJsAAEAAAAACERhdGFiYXNlVXNlSW50ZWdyYXRlZFNlY3VyaXR5AAACRGF0YWJhc2VVc2VyTmFtZQABAAAAAAhFbXVsYXRlQ2xvdWRTZWN1cml0eQABAkV4dHJhSnZtUGFyYW1ldGVycwABAAAAABBIdHRwUG9ydE51bWJlcgCQHwAAEE1heEphdmFIZWFwU2l6ZQAAAAAAAk5hbWUACAAAAERlZmF1bHQACE9wZW5BZG1pblBvcnQAAQhPcGVuSHR0cFBvcnQAABBTZXJ2ZXJQb3J0TnVtYmVyAJofAAAAAAADNACtAQAABSRJRAAQAAAAAGWBmiNWUjxTsFUwDkCaUAECJFR5cGUAFwAAAFNldHRpbmdzJE1vZGVsU2V0dGluZ3MAAkFmdGVyU3RhcnR1cE1pY3JvZmxvdwAnAAAAQXBwQ2xvdWRTZXJ2aWNlcy5TdGFydEFwcENsb3VkU2VydmljZXMACEFsbG93VXNlck11bHRpcGxlU2Vzc2lvbnMAAQJCZWZvcmVTaHV0ZG93bk1pY3JvZmxvdwABAAAAAAJEZWZhdWx0VGltZVpvbmVDb2RlAAEAAAAACEVuYWJsZURhdGFTdG9yYWdlT3B0aW1pc3RpY0xvY2tpbmcAAAhFbmZvcmNlRGF0YVN0b3JhZ2VVbmlxdWVuZXNzAAACRmlyc3REYXlPZldlZWsACAAAAERlZmF1bHQAAkhhc2hBbGdvcml0aG0ABwAAAEJDcnlwdAACSGVhbHRoQ2hlY2tNaWNyb2Zsb3cAAQAAAAACUm91bmRpbmdNb2RlAAcAAABIYWxmVXAAAlNjaGVkdWxlZEV2ZW50VGltZVpvbmVDb2RlAAgAAABFdGMvVVRDAAADNQBkAAAABSRJRAAQAAAAAHcOIxg+deRbn5o+ghgleSkCJFR5cGUAHAAAAFNldHRpbmdzJENvbnZlbnRpb25TZXR0aW5ncwAITG93ZXJDYXNlTWljcm9mbG93VmFyaWFibGVzAAAAAzYAIwEAAAUkSUQAEAAAAADDsyqg03mLW6xJOo+KaIpQAiRUeXBlABoAAABTZXR0aW5ncyRMYW5ndWFnZVNldHRpbmdzAAJEZWZhdWx0TGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwAETGFuZ3VhZ2VzALUAAAAQMAADAAAAAzEApgAAAAUkSUQAEAAAAACqtmOJ00tuT48LFIDYDPRUAiRUeXBlAA8AAABUZXh0cyRMYW5ndWFnZQAIQ2hlY2tDb21wbGV0ZW5lc3MAAAJDb2RlAAYAAABlbl9VUwACQ3VzdG9tRGF0ZUZvcm1hdAABAAAAAAJDdXN0b21EYXRlVGltZUZvcm1hdAABAAAAAAJDdXN0b21UaW1lRm9ybWF0AAEAAAAAAAAAAzcAYQAAAAUkSUQAEAAAAACEUrRNWZ4FVZVUbGnMf2GOAiRUeXBlAB0AAABTZXR0aW5ncyRDZXJ0aWZpY2F0ZVNldHRpbmdzAARDZXJ0aWZpY2F0ZXMADAAAABAwAAMAAAAAAAM4AGgAAAAFJElEABAAAAAA9WkgnFcd0Fy3SoSqiMzbyAIkVHlwZQAdAAAAU2V0dGluZ3MkSmF2YUFjdGlvbnNTZXR0aW5ncwAIR2VuZXJhdGVQb3N0Zml4ZXNGb3JQYXJhbWV0ZXJzAAEAAAA=" + }, + { + "unit_id": "b62986ab-1ec9-5c84-9d33-1f0829ffce9b", + "container_id": "5714121f-eb32-470e-ac23-9dedf56439b6", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Navigation$NavigationDocument", + "contents": "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" + }, + { + "unit_id": "57d2ecb5-6c2f-4ffa-a0b3-a37a40da3adf", + "container_id": "5714121f-eb32-470e-ac23-9dedf56439b6", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Texts$SystemTextCollection", + "contents": "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bGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AFgAAABZb3VyIHNlc3Npb24gaGFzIGV4cGlyZWQuIFBsZWFzZSBlbnRlciB5b3VyIHVzZXIgbmFtZSBhbmQgcGFzc3dvcmQgdG8gY29udGludWUgd29ya2luZy4AAAMyALIAAAAFJElEABAAAAAAlIzResy1IVeF4MizC44PcgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAVAAAAFV3IHNlc3NpZSBpcyB2ZXJsb3Blbi4gVm9lciB1dyBnZWJydWlrZXJzbmFhbSBlbiB3YWNodHdvb3JkIGluIG9tIHZlcmRlciB0ZSB3ZXJrZW4uAAAAAAADNDkA8QEAAAUkSUQAEAAAAAD746UeVn7bSLd6wYkw4tOwAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAWAAAAbWVuZGl4LnNpZ25pbi5odHRwNDAyAANUZXh0AIkBAAAFJElEABAAAAAAiP5HKiPRTkK+Gkayimh/6QIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMATQEAABAwAAMAAAADMQCYAAAABSRJRAAQAAAAAKbu8O+xLD1ainqonkE3+XUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADoAAABUaGUgY3VycmVudCBsaWNlbnNlIGRvZXMgbm90IGFsbG93IG1vcmUgdXNlcnMgdG8gc2lnbiBpbi4AAAMyAKMAAAAFJElEABAAAAAAlGrKkteI9VCX6zRaNirjswIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQARQAAAERlIGh1aWRpZ2UgbGljZW50aWUgc3RhYXQgbmlldCB0b2UgZGF0IGVyIG1lZXIgZ2VicnVpa2VycyBhYW5tZWxkZW4uAAAAAAADNTAAlAEAAAUkSUQAEAAAAAAGEb9GotcXVI3aQ+g9KxctAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAhAAAAbWVuZGl4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aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADEAAABQYXNzd29yZCBkb2VzIG5vdCBtZWV0IHBhc3N3b3JkIHBvbGljeSBjcml0ZXJpYToAAAMyAJQAAAAFJElEABAAAAAAh3dOKN/FClyvNkqW/tXUxgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQANgAAAEhldCB3YWNodHdvb3JkIHZvbGRvZXQgbmlldCBhYW4gaGV0IHdhY2h0d29vcmRiZWxlaWQ6AAAAAAADNjcA3gEAAAUkSUQAEAAAAACaQnhU71+vU4mGp9C82I60AiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAhAAAAbWVuZGl4LnBsYXRmb3JtLnBhc3N3b3JkVG9vU2hvcnQAA1RleHQAawEAAAUkSUQAEAAAAABlheWZwy+6VpbJ5mUR40FJAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAvAQAAEDAAAwAAAAMxAIoAAAAFJElEABAAAAAAYeSm5apgSlqu8UmkE/JvWAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALAAAAFBhc3N3b3JkIHNob3VsZCBiZSBhdCBsZWFzdCB7MX0gY2hhcmFjdGVycy4AAAMyAJMAAAAFJElEABAAAAAA15sYB9VV4VWhxp30oeFwEAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQANQAAAEhldCB3YWNodHdvb3JkIG1vZXQgbWluaW1hYWwgezF9IGthcmFrdGVycyBiZXZhdHRlbi4AAAAAAAM2OADDAQAABSRJRAAQAAAAAOIXK1fke/1dsNE7c582a9UCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB0AAABtZW5kaXgucGxhdGZvcm0ubWlzc2luZ0RpZ2l0AANUZXh0AFQBAAAFJElEABAAAAAA7qZLwvuH1lqG7R5LXSdnQwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAGAEA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AAAFJElEABAAAAAAMPlY/GbnnVCqyn/s7Hl6+gIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAYAAAAERlY2ltYWwgdmFsdWVzIG1heSBjb250YWluIG5vIG1vcmUgdGhhbiAyMCBkaWdpdHMgYmVmb3JlIGFuZCA4IGRpZ2l0cyBiZWhpbmQgdGhlIGRlY2ltYWwgcG9pbnQuAAADMgDAAAAABSRJRAAQAAAAAFT9sqxahNNVrh0bEiFowdACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AGIAAABEZWNpbWFsZSBnZXRhbGxlbiBtb2V0ZW4gbWluZGVyIGdldGFsbGVuIGJldmF0dGVuIGRhbiAyMCBkZWNpbWFsZW4gdm9vciBlbiA4IGdldGFsbGVuIG5hIGRlIHB1bnQuAAAAAAADODQAAgIAAAUkSUQAEAAAAABoTwJgJAj/X6VkRi9znIoYAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAkAAAAbWVuZGl4LmxpYi5WYWxpZGF0aW9ucy5yYW5nZV9udW1iZXIAA1RleHQAjAEAAAUkSUQAEAAAAAAQXaQqBvZHULLHkEJPGNgjAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwBQAQAAEDAAAwAAAAMxAJsAAAAFJElEABAAAAAAZTE/rHak3VeOW3xZ+ySWDAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAPQAAAFRoaXMgbnVtYmVyIGlzIHRvbyBiaWcgYW5kIGNhbm5vdCBiZSBhY2N1cmF0ZWx5IHJlcHJlc2VudGVkLgAAAzIAowAAAAUkSUQAEAAAAAClQFmaAPVLW6D2bc1jdB5TAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABFAAAARGl0IGdldGFsIGlzIHRlIGdyb290IGVuIGthbiBuaWV0IG5hdXdrZXVyaWcgd29yZGVuIGdlcmVwcmVzZW50ZWVyZC4A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AAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAbQAAAFUgaGVidCBuaWV0IGdlbm9lZyByZWNodGVuIG9tIGRlemUgcGFnaW5hIHRlIG9wZW5lbi4gVSBrdW50IHByb2JlcmVuIGFhbiB0ZSBtZWxkZW4gYWxzIGVlbiBhbmRlcmUgZ2VicnVpa2VyLgAAAAAAAzkzAKcBAAAFJElEABAAAAAAvVOLqIdXkVe9RZcVPQH+UwIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAKQAAAG1lbmRpeC53aWRnZXQuRGF0ZVBpY2tlci5vcGVuX2RhdGVwaWNrZXIAA1RleHQALAEAAAUkSUQAEAAAAABN8v9MSn4oXoFJnj+3jFklAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDwAAAAEDAAAwAAAAMxAG8AAAAFJElEABAAAAAAlJG2dpN6g1C8iRfOQEAm2QIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAEQAAAFNob3cgZGF0ZSBwaWNrZXIAAAMyAG8AAAAFJElEABAAAAAAAOtfzTo+t164f/WL2ryaKAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAEQAAAFRvb24gZGF0dW1raWV6ZXIAAAAAAAAA" + }, + { + "unit_id": "bd6f65bd-2393-539d-9388-4684a54d0646", + "container_id": "5714121f-eb32-470e-ac23-9dedf56439b6", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Security$ProjectSecurity", + "contents": "VAQAAAUkSUQAEAAAAAC9ZW+9kyOdU5OIRoSlTQZGAiRUeXBlABkAAABTZWN1cml0eSRQcm9qZWN0U2VjdXJpdHkAAkFkbWluUGFzc3dvcmQAAgAAADEAAkFkbWluVXNlck5hbWUACAAAAE14QWRtaW4AAkFkbWluVXNlclJvbGUAAQAAAAAIQ2hlY2tTZWN1cml0eQABBERlbW9Vc2VycwCSAQAAEDAAAgAAAAMxAMkAAAAFJElEABAAAAAAD8QgBhnLAECUGViBCTkP7gIkVHlwZQAWAAAAU2VjdXJpdHkkRGVtb1VzZXJJbXBsAAJFbnRpdHkAFwAAAEFkbWluaXN0cmF0aW9uLkFjY291bnQAAlBhc3N3b3JkAAsAAABrS1JFeWJHQTc3AAJVc2VyTmFtZQATAAAAZGVtb19hZG1pbmlzdHJhdG9yAARVc2VyUm9sZXMAIQAAABAwAAEAAAACMQAOAAAAQWRtaW5pc3RyYXRvcgAAAAMyALcAAAAFJElEABAAAAAAZT44j85+VEeIrehkQObKCgIkVHlwZQAWAAAAU2VjdXJpdHkkRGVtb1VzZXJJbXBsAAJFbnRpdHkAFwAAAEFkbWluaXN0cmF0aW9uLkFjY291bnQAAlBhc3N3b3JkAAsAAABSd0pWV05NSTAxAAJVc2VyTmFtZQAKAAAAZGVtb191c2VyAARVc2VyUm9sZXMAGAAAABAwAAEAAAACMQAFAAAAVXNlcgAAAAAIRW5hYmxlRGVtb1VzZXJzAAEIRW5hYmxlR3Vlc3RBY2Nlc3MAAANGaWxlRG9jdW1lbnRBY2Nlc3MAbAAAAAUkSUQAEAAAAAAIduuNSeGKRp/4JDU20NuvAiRUeXBlACkAAABTZWN1cml0eSRGaWxlRG9jdW1lbnRBY2Nlc3NSdWxlQ29udGFpbmVyAARBY2Nlc3NSdWxlcwAMAAAAEDAAAwAAAAAAAkd1ZXN0VXNlclJvbGUAAQAAAAADSW1hZ2VBY2Nlc3MAZQAAAAUkSUQAEAAAAABRZ3yQBxdRRKLK3P7Cb5Y+AiRUeXBlACIAAABTZWN1cml0eSRJbWFnZUFjY2Vzc1J1bGVDb250YWluZXIABEFjY2Vzc1J1bGVzAAwAAAAQMAADAAAAAAADUGFzc3dvcmRQb2xpY3lTZXR0aW5ncwCPAAAABSRJRAAQAAAAAP19SHgAaVBFuvXItKSIgrgCJFR5cGUAIAAAAFNlY3VyaXR5JFBhc3N3b3JkUG9saWN5U2V0dGluZ3MAEE1pbmltdW1MZW5ndGgABgAAAAhSZXF1aXJlRGlnaXQAAQhSZXF1aXJlTWl4ZWRDYXNlAAEIUmVxdWlyZVN5bWJvbAAAAAJTZWN1cml0eUxldmVsAA0AAABDaGVja05vdGhpbmcAAlNpZ25Jbk1pY3JvZmxvdwABAAAAAARVc2VyUm9sZXMADAAAABAwAAIAAAAAAA==" + }, + { + "unit_id": "1fcc2823-c303-5a27-b6c7-78b04c96c88f", + "container_id": "5714121f-eb32-470e-ac23-9dedf56439b6", + "module": null, + "containment": "ProjectConversion", + "name": "", + "type": "Projects$ProjectConversion", + "contents": "uhcAAAUkSUQAEAAAAAAjKMwfA8MnWrbHeLBMlsiPAiRUeXBlABsAAABQcm9qZWN0cyRQcm9qZWN0Q29udmVyc2lvbgAET25lVGltZUNvbnZlcnNpb25zAGEXAAAQMAADAAAAAzEAagAAAAUkSUQAEAAAAABgh+VGzLKwX4DHvEWFciIEAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAbAAAARGF0YVR5cGVUb014VHlwZUNvbnZlcnNpb24AAAMyAHQAAAAFJElEABAAAAAAXXWCgmqgaFKxJ7O8eLTowAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAFBhcmFtZXRlck14VHlwZVRvQmFzaWNUeXBlQ29udmVyc2lvbgAAAzMAfgAAAAUkSUQAEAAAAABKZ01/le4vXbilsr77D8nUAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAvAAAAUGFyYW1ldGVyTWFwcGluZ0FyZ3VtZW50VG9CYXNpY1ZhbHVlQ29udmVyc2lvbgAAAzQAfgAAAAUkSUQAEAAAAABALfUN6LGFUKNUAXo27hC6AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAvAAAAQ29udmVydEluaGVyaXRhbmNlUGFyYW1ldGVyTmFtZUluRXhwb3J0TWFwcGluZwAAAzUAeQAAAAUkSUQAEAAAAACU6DgsBwwwXaQNvGUVFAczAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAqAAAAQ29udmVydEluaGVyaXRhbmNlQ29tcGxleFBhcmFtZXRlck1hcHBpbmcAAAM2AGYAAAAFJElEABAAAAAAO8d6MStm01uLEqtgCb1SYwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFwAAAENvbnZlcnRJbmhlcml0YW5jZVBhdGgAAAM3AHsAAAAFJElEABAAAAAAwVpEHNS9NVSwGrgVdohsOQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUALAAAAENvbnZlcnRFeHBvcnRNYXBwaW5nRm9yQ2hvaWNlT2JqZWN0SGFuZGxpbmcAAAM4AG4AAAAFJElEABAAAAAAUe6JZK21dFirTJkoM9E2wwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAENvbnZlcnRDaG9pY2VFbGVtZW50T2NjdXJyZW5jZQAAAzkAcwAAAAUkSUQAEAAAAADLlUG+fnuCUbNBZ1f1FtP/AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAAQ29udmVydEluaGVyaXRhbmNlRWxlbWVudE9jY3VycmVuY2UAAAMxMABnAAAABSRJRAAQAAAAAG1JEB62/KRdtvhVckvFrY8CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABgAAABDb252ZXJ0WG1sUHJpbWl0aXZlVHlwZQAAAzExAHQAAAAFJElEABAAAAAA/hNzuOq8oVKe17JU3OWalQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAENvbnZlcnRYbWxQcmltaXRpdmVUeXBlSW50ZWdlclRvTG9uZwAAAzEyAHQAAAAFJElEABAAAAAAXhCypAZZqFufplg8CEoyigIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJQAAAENvbnZlcnRDaG9pY2VJbmhlcml0YW5jZUFzc29jaWF0aW9ucwAAAzEzAGQAAAAFJElEABAAAAAA3I701Oc4c1G1kK4h9ShxuwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFQAAAENvbnZlcnRJc0RlZmF1bHRUeXBlAAADMTQAdAAAAAUkSUQAEAAAAACiZuF7YbMwV5NFr5RvMbzqAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAlAAAAQ29udmVydEV4cGVjdGVkQ29udGVudFR5cGVzVG9Vc2VNdG9tAAADMTUAZgAAAAUkSUQAEAAAAAAS8E2X6gJTXLF6yOP+/rzQAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAXAAAAQ29udmVydFhtbEV4cG9zZWROYW1lcwAAAzE2AGsAAAAFJElEABAAAAAAzMzyowVQtVGP2h9Krv/f2QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAENvbnZlcnRKc29uRWxlbWVudHNPcHRpb25hbAAAAzE3AGsAAAAFJElEABAAAAAAoG3S4HucNFmBkfkmdgNkTgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAENvbnZlcnRKc29uRWxlbWVudHNOaWxsYWJsZQAAAzE4AGYAAAAFJElEABAAAAAAR7wixa+H7Vq/liD5nidegAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAFwAAAENvbnZlcnROdWxsVmFsdWVPcHRpb24AAAMxOQBsAAAABSRJRAAQAAAAAJ+x4zWNxPNRm8XE6UaZ3GUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB0AAABDb252ZXJ0U3lzdGVtSWREYXRhQXR0cmlidXRlAAADMjAAcgAAAAUkSUQAEAAAAAD3qJuhKW6NX5FrRLQLP/ojAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAjAAAAQ2xlYXJJbnZpc2libGVTZWFyY2hCYXJzQ29udmVyc2lvbgAAAzIxAGsAAAAFJElEABAAAAAAvyXQLKhn8VWbsU38EVVu7gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAE5ld0J1dHRvbkVudGl0eVRvRW50aXR5UGF0aAAAAzIyAHUAAAAFJElEABAAAAAAdpWXrFfGp1+VhgOcC3guNAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJgAAAFNldERhdGFWaWV3Q29udHJvbEJhclRvTnVsbENvbnZlcnNpb24AAAMyMwB0AAAABSRJRAAQAAAAAK/PiY3C+XJZjDTzqx4Jm3ACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACUAAABEYXRhVmlld0NvbnRyb2xCYXJUb0Zvb3RlckNvbnZlcnNpb24AAAMyNABtAAAABSRJRAAQAAAAABTelvwqGrhWnzCXYGrz9b8CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB4AAABMYXlvdXRDYWxsT3B0aW9uYWxDb252ZXJzaW9uIAAAAzI1AHcAAAAFJElEABAAAAAARqxZR/9BqlShICYfhTr2lQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKAAAAFBsYWNlaG9sZGVySWRlbnRpZmllcnNUb05hbWVzQ29udmVyc2lvbgAAAzI2AHYAAAAFJElEABAAAAAA115YfHtdwFqVAhrABffugQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAExheW91dENhbGxBcmd1bWVudElkZW50aWZpZXJDb252ZXJzaW9uAAADMjcAdQAAAAUkSUQAEAAAAAApoQKXwFKOVa2EIQ7z7uVHAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAUmVtb3ZlRHVwbGljYXRlVHJhbnNsYXRpb25zQ29udmVyc2lvbgAAAzI4AGwAAAAFJElEABAAAAAAhv6T0OWCG1CA7yQus9LObwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHQAAAEphdmFBY3Rpb25TZXR0aW5nc0NvbnZlcnNpb24AAAMyOQB3AAAABSRJRAAQAAAAAJom5Fei2HNbo1oqaktLATUCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACgAAABDb252ZXJ0UmVzdENhbGxBY3Rpb25SZXN1bHRIYW5kbGluZ1R5cGUAAAMzMAB4AAAABSRJRAAQAAAAAD2CT0qugalZp//nLI2NPU0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACkAAABDb252ZXJ0UmVzdENhbGxBY3Rpb25SZXF1ZXN0SGFuZGxpbmdUeXBlAAADMzEAbQAAAAUkSUQAEAAAAAD8u5jw3e74X6qFYBABB0ysAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAQ29udmVydFJlc3VsdEhhbmRsaW5nRGF0YVR5cGUAAAMzMgBqAAAABSRJRAAQAAAAAO7kuGIYo7NfnNM3ZsbM/kkCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABsAAABNZW51UGFnZVNldHRpbmdzQ29udmVyc2lvbgAAAzMzAGcAAAAFJElEABAAAAAADrc+9SBZSV2hnzOzYrM9GQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGAAAAFJlYWRPbmx5U3R5bGVDb252ZXJzaW9uAAADMzQAbQAAAAUkSUQAEAAAAAD2tKQ036hcWpSX0FERzE4QAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAU2lkZWJhclRvZ2dsZUJ1dHRvbkNvbnZlcnNpb24AAAMzNQBqAAAABSRJRAAQAAAAAGiV3wRleD5dtGWNahKn+I4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABsAAABSZW1vdmVSZXBvcnRQYW5lQ29udmVyc2lvbgAAAzM2AG8AAAAFJElEABAAAAAAEFk/Q5r6LVmc8TsMwP814gIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIAAAAERhdGFzdG9yYWdlVW5pcXVlbmVzc0NvbnZlcnNpb24AAAMzNwBpAAAABSRJRAAQAAAAAOilZvys+IpSloECcPS3RU4CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABoAAABDb252ZXJ0V2Vic2VydmljZU5vUmVzdWx0AAADMzgAZAAAAAUkSUQAEAAAAAAaTuB9pBWFU56qQbmZXKI0AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAVAAAAU3RhdGlzdGljc0NvbnZlcnNpb24AAAMzOQBrAAAABSRJRAAQAAAAACLz1hiGABpSnZmzbyTuQN0CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABPbmVUb3BsZXZlbEJ1dHRvbkNvbnZlcnNpb24AAAM0MABsAAAABSRJRAAQAAAAAKPq7M/9EUxUsAtDYJR326UCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB0AAABIeWJyaWRQaG9uZVByb2ZpbGVDb252ZXJzaW9uAAADNDEAbQAAAAUkSUQAEAAAAAAzEV7XYyRhVL9a0J3kc120AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAeAAAAT2xkSHlicmlkUGhvbmVUb05ld0NvbnZlcnNpb24AAAM0MgBmAAAABSRJRAAQAAAAANX8yIMKEoxWraSTLK2v8hYCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABcAAABMb2dvdXRCdXR0b25Db252ZXJzaW9uAAADNDMAbAAAAAUkSUQAEAAAAACX4bonXcdTWZg8+A3Z+onxAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAdAAAATW92ZVByb2ZpbGVzVG9MaXN0Q29udmVyc2lvbgAAAzQ0AG4AAAAFJElEABAAAAAAsqNxb4pqUVSctS6Iq2cEqAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAFJlbW92ZVVudXNlZFByb2ZpbGVzQ29udmVyc2lvbgAAAzQ1AHMAAAAFJElEABAAAAAAQVg7+4YqUlCKA6p0Vt7boQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJAAAAFNldFRhYmxldEFuZFBob25lSG9tZVBhZ2VDb252ZXJzaW9uAAADNDYAegAAAAUkSUQAEAAAAAD3dV5f1VdTW5tqgeCTaOWgAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQArAAAARml4SURzT2ZEcm9wRG93bkVtcHR5T3B0aW9uVGV4dHNDb252ZXJzaW9uAAADNDcAZAAAAAUkSUQAEAAAAADNRtaNXSfRV6lBLmuwveN7AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAVAAAATGlua0J1dHRvbkNvbnZlcnNpb24AAAM0OABmAAAABSRJRAAQAAAAABamavX7kjpUkeryo6vPWEECJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABcAAABGaWxlRG9jdW1lbnRDb252ZXJzaW9uAAADNDkAdQAAAAUkSUQAEAAAAABlY8lbqn+LWKQe/2eZfEIaAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAmAAAAQ29udmVydFdzZGxWYWxpZGF0ZVNjaGVtYU5vdFN1cHBvcnRlZAAAAzUwAGsAAAAFJElEABAAAAAACDOgmh5+B12NFGS4ll7wSAIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHAAAAEV4dGVuZFByb2ZpbGVLaW5kQ29udmVyc2lvbgAAAzUxAHYAAAAFJElEABAAAAAA8CVX97GAyFO3srAlfWSHtgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAERhdGFTdG9yYWdlT3B0aW1pc3RpY0xvY2tpbmdDb252ZXJzaW9uAAADNTIAcgAAAAUkSUQAEAAAAABXcevb6eulWrCNTZwl/oKDAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAjAAAAQ29udmVydFJvb3RFbGVtZW50UGFyYW1ldGVyTWFwcGluZwAAAAA=" + } + ] +} diff --git a/lib/mxrb/templates/project/9.6.1.29396.json b/lib/mxrb/templates/project/9.6.1.29396.json new file mode 100644 index 0000000..fbeeaa0 --- /dev/null +++ b/lib/mxrb/templates/project/9.6.1.29396.json @@ -0,0 +1,50 @@ +{ + "format_version": "v1", + "units": [ + { + "unit_id": "4e70063d-53e1-477a-ab92-6b763330c293", + "container_id": "82d1d7d0-ca49-4703-8557-1f478ee46d7c", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Settings$ProjectSettings", + "contents": "+AcAAAUkSUQAEAAAAAA9BnBO4VN6R6uSa3YzMMKTAiRUeXBlABkAAABTZXR0aW5ncyRQcm9qZWN0U2V0dGluZ3MABFNldHRpbmdzAKsHAAAQMAACAAAAAzEAvgAAAAUkSUQAEAAAAAC82XyoYWY3WqFPOBeQlP/sAiRUeXBlAB8AAABGb3JtcyRXZWJVSVByb2plY3RTZXR0aW5nc1BhcnQACEVuYWJsZURvd25sb2FkUmVzb3VyY2VzAAAIRW5hYmxlTWljcm9mbG93UmVhY2hhYmlsaXR5QW5hbHlzaXMAAQhFbmFibGVXaWRnZXRCdW5kbGluZwAABFRoZW1lTW9kdWxlT3JkZXIADAAAABAwAAMAAAAAAAMyAFIAAAAFJElEABAAAAAAIK4HHOsrZFCkxPyYZsoZ5wIkVHlwZQAoAAAAU2V0dGluZ3MkSW50ZWdyYXRpb25Qcm9qZWN0U2V0dGluZ3NQYXJ0AAADMwAyAgAABSRJRAAQAAAAANgEzc8vM7FcjGTT8YSkdzECJFR5cGUAHwAAAFNldHRpbmdzJENvbmZpZ3VyYXRpb25TZXR0aW5ncwAEQ29uZmlndXJhdGlvbnMA2QEAABAwAAMAAAADMQDKAQAABSRJRAAQAAAAAD+J/LZBqrZBpQdNc2qwJcYCJFR5cGUAHQAAAFNldHRpbmdzJFNlcnZlckNvbmZpZ3VyYXRpb24AAkFwcGxpY2F0aW9uUm9vdFVybAAXAAAAaHR0cDovL2xvY2FsaG9zdDo4MDgwLwAEQ29uc3RhbnRWYWx1ZXMADAAAABAwAAMAAAAABEN1c3RvbVNldHRpbmdzAAwAAAAQMAADAAAAAAJEYXRhYmFzZU5hbWUACAAAAGRlZmF1bHQAAkRhdGFiYXNlUGFzc3dvcmQAAQAAAAACRGF0YWJhc2VUeXBlAAcAAABIc3FsZGIAAkRhdGFiYXNlVXJsAAEAAAAACERhdGFiYXNlVXNlSW50ZWdyYXRlZFNlY3VyaXR5AAACRGF0YWJhc2VVc2VyTmFtZQABAAAAAAJFeHRyYUp2bVBhcmFtZXRlcnMAAQAAAAAQSHR0cFBvcnROdW1iZXIAkB8AABBNYXhKYXZhSGVhcFNpemUAAAAAAAJOYW1lAAgAAABEZWZhdWx0AAhPcGVuQWRtaW5Qb3J0AAEIT3Blbkh0dHBQb3J0AAAQU2VydmVyUG9ydE51bWJlcgCaHwAAAAAAAzQAjQEAAAUkSUQAEAAAAABlgZojVlI8U7BVMA5AmlABAiRUeXBlABcAAABTZXR0aW5ncyRNb2RlbFNldHRpbmdzAAJBZnRlclN0YXJ0dXBNaWNyb2Zsb3cAAQAAAAAIQWxsb3dVc2VyTXVsdGlwbGVTZXNzaW9ucwABAkJlZm9yZVNodXRkb3duTWljcm9mbG93AAEAAAAAAkRlZmF1bHRUaW1lWm9uZUNvZGUAAQAAAAAIRW5hYmxlRGF0YVN0b3JhZ2VPcHRpbWlzdGljTG9ja2luZwAAAkZpcnN0RGF5T2ZXZWVrAAgAAABEZWZhdWx0AAJIYXNoQWxnb3JpdGhtAAcAAABCQ3J5cHQAAkhlYWx0aENoZWNrTWljcm9mbG93AAEAAAAAAlJvdW5kaW5nTW9kZQAHAAAASGFsZlVwAAJTY2hlZHVsZWRFdmVudFRpbWVab25lQ29kZQAIAAAARXRjL1VUQwAIVXNlU3lzdGVtQ29udGV4dEZvckJhY2tncm91bmRUYXNrcwAAAAM1AI0AAAAFJElEABAAAAAAdw4jGD515Fufmj6CGCV5KQIkVHlwZQAcAAAAU2V0dGluZ3MkQ29udmVudGlvblNldHRpbmdzAARBY3Rpb25BY3Rpdml0eURlZmF1bHRDb2xvcnMADAAAABAwAAMAAAAACExvd2VyQ2FzZU1pY3JvZmxvd1ZhcmlhYmxlcwAAAAM2ACMBAAAFJElEABAAAAAAw7MqoNN5i1usSTqPimiKUAIkVHlwZQAaAAAAU2V0dGluZ3MkTGFuZ3VhZ2VTZXR0aW5ncwACRGVmYXVsdExhbmd1YWdlQ29kZQAGAAAAZW5fVVMABExhbmd1YWdlcwC1AAAAEDAAAwAAAAMxAKYAAAAFJElEABAAAAAA9ZVS7t+9bEeNH/5m6eYh0AIkVHlwZQAPAAAAVGV4dHMkTGFuZ3VhZ2UACENoZWNrQ29tcGxldGVuZXNzAAACQ29kZQAGAAAAZW5fVVMAAkN1c3RvbURhdGVGb3JtYXQAAQAAAAACQ3VzdG9tRGF0ZVRpbWVGb3JtYXQAAQAAAAACQ3VzdG9tVGltZUZvcm1hdAABAAAAAAAAAAM3AGEAAAAFJElEABAAAAAAhFK0TVmeBVWVVGxpzH9hjgIkVHlwZQAdAAAAU2V0dGluZ3MkQ2VydGlmaWNhdGVTZXR0aW5ncwAEQ2VydGlmaWNhdGVzAAwAAAAQMAADAAAAAAADOACnAAAABSRJRAAQAAAAAM8Cnn78AhRTkkR5zHrpd2cCJFR5cGUAJgAAAFNldHRpbmdzJFdvcmtmbG93c1Byb2plY3RTZXR0aW5nc1BhcnQAEERlZmF1bHRUYXNrUGFyYWxsZWxpc20AAwAAAAJVc2VyRW50aXR5AAwAAABTeXN0ZW0uVXNlcgAQV29ya2Zsb3dFbmdpbmVQYXJhbGxlbGlzbQAFAAAAAAAA" + }, + { + "unit_id": "b62986ab-1ec9-5c84-9d33-1f0829ffce9b", + "container_id": "82d1d7d0-ca49-4703-8557-1f478ee46d7c", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Navigation$NavigationDocument", + "contents": "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zqp4+gIkVHlwZQAPAAAATWVudXMkTWVudUl0ZW0AA0FjdGlvbgAVAQAABSRJRAAQAAAAAKslyxVZW1xCocHMpx6pdWUCJFR5cGUAEQAAAEZvcm1zJEZvcm1BY3Rpb24ACERpc2FibGVkRHVyaW5nRXhlY3V0aW9uAAEDRm9ybVNldHRpbmdzAHEAAAAFJElEABAAAAAAVfRV4Uu4j0SaH9zVX1cZTgIkVHlwZQATAAAARm9ybXMkRm9ybVNldHRpbmdzAAJGb3JtABsAAABNeUZpcnN0TW9kdWxlLkxvY2F0aW9uUGFnZQAKVGl0bGVPdmVycmlkZQAAAk51bWJlck9mUGFnZXNUb0Nsb3NlMgABAAAAAARQYWdlc0ZvclNwZWNpYWxpemF0aW9ucwAMAAAAEDAAAgAAAAAACkFsdGVybmF0aXZlVGV4dAADQ2FwdGlvbgCzAAAABSRJRAAQAAAAAPkaU7qNpHJCtPeEDVW5RN8CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAHcAAAAQMAADAAAAAzEAaAAAAAUkSUQAEAAAAAB0QTQ3zavoQYe0JO7U3HPWAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAKAAAATG9jYXRpb25zAAAAAANJY29uAEQAAAAFJElEABAAAAAAYSmCByK/uE+ltyWwZQcbfwIkVHlwZQAQAAAARm9ybXMkR2x5cGhJY29uABBDb2RlAGLgAAAABEl0ZW1zAAwAAAAQMAADAAAAAAADNQCGAgAABSRJRAAQAAAAAGhQKyHpCw5BiDM4tRRyhqwCJFR5cGUADwAAAE1lbnVzJE1lbnVJdGVtAANBY3Rpb24AGAEAAAUkSUQAEAAAAAB3jlLB5PfER6u1J/dydsazAiRUeXBlABEAAABGb3JtcyRGb3JtQWN0aW9uAAhEaXNhYmxlZER1cmluZ0V4ZWN1dGlvbgABA0Zvcm1TZXR0aW5ncwB0AAAABSRJRAAQAAAAAN2hizzzUXVBhvPCeQzzrOECJFR5cGUAEwAAAEZvcm1zJEZvcm1TZXR0aW5ncwACRm9ybQAeAAAATXlGaXJzdE1vZHVsZS5TYWxlc1BlcnNvblBhZ2UAClRpdGxlT3ZlcnJpZGUAAAJOdW1iZXJPZlBhZ2VzVG9DbG9zZTIAAQAAAAAEUGFnZXNGb3JTcGVjaWFsaXphdGlvbnMADAAAABAwAAIAAAAAAApBbHRlcm5hdGl2ZVRleHQAA0NhcHRpb24AtgAAAAUkSUQAEAAAAABi+LyISxMwSLhQ+duDhUyfAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwB6AAAAEDAAAwAAAAMxAGsAAAAFJElEABAAAAAA8ltcsARPgEqWi21jWuj5EQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQADQAAAFNhbGVzIFBlcnNvbgAAAAADSWNvbgBEAAAABSRJRAAQAAAAACRMfoWpGmVMsIpQ6lBXRYsCJFR5cGUAEAAAAEZvcm1zJEdseXBoSWNvbgAQQ29kZQCE4QAAAARJdGVtcwAMAAAAEDAAAwAAAAAAAzYAjQIAAAUkSUQAEAAAAABSen246FqHQYMunMWD79GRAiRUeXBlAA8AAABNZW51cyRNZW51SXRlbQADQWN0aW9uABoBAAAFJElEABAAAAAAkNpBI8pBPkaiD3kHA3qAAwIkVHlwZQARAAAARm9ybXMkRm9ybUFjdGlvbgAIRGlzYWJsZWREdXJpbmdFeGVjdXRpb24AAQNGb3JtU2V0dGluZ3MAdgAAAAUkSUQAEAAAAAArEZb5ZH15R6W2pXFh1dSKAiRUeXBlABMAAABGb3JtcyRGb3JtU2V0dGluZ3MAAkZvcm0AIAAAAE15Rmlyc3RNb2R1bGUuU2FsZXNUcmFja2luZ1BhZ2UAClRpdGxlT3ZlcnJpZGUAAAJOdW1iZXJPZlBhZ2VzVG9DbG9zZTIAAQAAAAAEUGFnZXNGb3JTcGVjaWFsaXphdGlvbnMADAAAABAwAAIAAAAAAApBbHRlcm5hdGl2ZVRleHQAA0NhcHRpb24AuwAAAAUkSUQAEAAAAABdQqv+1WhsRZx8mkbJe119AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwB/AAAAEDAAAwAAAAMxAHAAAAAFJElEABAAAAAAstKSqRf6AEOvCd/5l+DemQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAEgAAAFNhbGVzIFRyYW5zYWN0aW9uAAAAAANJY29uAEQAAAAFJElEABAAAAAAByu1FxuME0eElHXn5FplQwIkVHlwZQAQAAAARm9ybXMkR2x5cGhJY29uABBDb2RlAEjhAAAABEl0ZW1zAAwAAAAQMAADAAAAAAAAAAJOYW1lAAsAAABSZXNwb25zaXZlAARPZmZsaW5lRW50aXR5Q29uZmlncwAMAAAAEDAAAgAAAAAKUHJvZ3Jlc3NpdmVXZWJBcHBTZXR0aW5ncwAAAzIASAUAAAUkSUQAEAAAAABNVDIgCYkBQ62Bt6zLUSB3AiRUeXBlAB0AAABOYXZpZ2F0aW9uJE5hdmlnYXRpb25Qcm9maWxlAAJBcHBJY29uAAEAAAAAA0FwcFRpdGxlALAAAAAFJElEABAAAAAAxJbQi9OkY0q8LjbSa4K1ZwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAdAAAABAwAAMAAAADMQBlAAAABSRJRAAQAAAAALkKbcVEuo9Aueg2Jv00H68CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAcAAABNZW5kaXgAAAAABEhvbWVJdGVtcwAMAAAAEDAAAgAAAAADSG9tZVBhZ2UAdQAAAAUkSUQAEAAAAADVXOK4d95YSacWg48JVQyMAiRUeXBlABQAAABOYXZpZ2F0aW9uJEhvbWVQYWdlAAJNaWNyb2Zsb3cAAQAAAAACUGFnZQAdAAAATXlGaXJzdE1vZHVsZS5Nb2JpbGVIb21lUGFnZQAAAktpbmQABgAAAFBob25lAANMb2dpblBhZ2VTZXR0aW5ncwBXAAAABSRJRAAQAAAAAOO/BgxFKe9BiCRi2kPxfNYCJFR5cGUAEwAAAEZvcm1zJEZvcm1TZXR0aW5ncwACRm9ybQABAAAAAApUaXRsZU92ZXJyaWRlAAADTWVudQDWAgAABSRJRAAQAAAAAHw97QHtNRtEvXI2WFiYG7oCJFR5cGUAGQAAAE1lbnVzJE1lbnVJdGVtQ29sbGVjdGlvbgAESXRlbXMAjAIAABAwAAMAAAADMQB9AgAABSRJRAAQAAAAADvnVVt7MxVAsVAx4hRVSaICJFR5cGUADwAAAE1lbnVzJE1lbnVJdGVtAANBY3Rpb24AFwEAAAUkSUQAEAAAAAD/9WF5nnbkRpurPiTTCHbDAiRUeXBlABEAAABGb3JtcyRGb3JtQWN0aW9uAAhEaXNhYmxlZER1cmluZ0V4ZWN1dGlvbgABA0Zvcm1TZXR0aW5ncwBzAAAABSRJRAAQAAAAAJCaVCgYQydKrOSwizUp8k0CJFR5cGUAEwAAAEZvcm1zJEZvcm1TZXR0aW5ncwACRm9ybQAdAAAATXlGaXJzdE1vZHVsZS5Nb2JpbGVIb21lUGFnZQAKVGl0bGVPdmVycmlkZQAAAk51bWJlck9mUGFnZXNUb0Nsb3NlMgABAAAAAARQYWdlc0ZvclNwZWNpYWxpemF0aW9ucwAMAAAAEDAAAgAAAAAACkFsdGVybmF0aXZlVGV4dAADQ2FwdGlvbgCuAAAABSRJRAAQAAAAAPME4EuLHrJFriy9/r1H4W4CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAHIAAAAQMAADAAAAAzEAYwAAAAUkSUQAEAAAAABW4ijAfXLWSKZyotKnMBfhAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAFAAAASG9tZQAAAAADSWNvbgBEAAAABSRJRAAQAAAAACENrjZjlzNIjP9pRHr2VnkCJFR5cGUAEAAAAEZvcm1zJEdseXBoSWNvbgAQQ29kZQAh4AAAAARJdGVtcwAMAAAAEDAAAwAAAAAAAAACTmFtZQAGAAAAUGhvbmUABE9mZmxpbmVFbnRpdHlDb25maWdzAAwAAAAQMAACAAAAAApQcm9ncmVzc2l2ZVdlYkFwcFNldHRpbmdzAAAAAA==" + }, + { + "unit_id": "57d2ecb5-6c2f-4ffa-a0b3-a37a40da3adf", + "container_id": "82d1d7d0-ca49-4703-8557-1f478ee46d7c", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Texts$SystemTextCollection", + "contents": "4KoAAAUkSUQAEAAAAAC17NJXL2z6T6Czo3pA2jrfAiRUeXBlABsAAABUZXh0cyRTeXN0ZW1UZXh0Q29sbGVjdGlvbgAEU3lzdGVtVGV4dHMAjqoAABAwAAIAAAADMQChAQAABSRJRAAQAAAAADx/VKeK0DJBpi4n1lr+1HoCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB8AAABtZW5kaXgubGliLk14RGF0YVNvdXJjZS5zdGF0dXMAA1RleHQAMAEAAAUkSUQAEAAAAACSTMf8s7VkTJGJXhYaLFzrAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwD0AAAAEDAAAwAAAAMxAHAAAAAFJElEABAAAAAAkX2vMQ+7Uli8179NSIhccQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAEgAAAHsxfSB0byB7Mn0gb2YgezN9AAADMgByAAAABSRJRAAQAAAAAOQaOs0vkYVVnPwOWL76WgUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ABQAAAB7MX0gdG90IHsyfSB2YW4gezN9AAAAAAADMgDSAQAABSRJRAAQAAAAAAV9uXwNJClAsMKjDz6Y6lYCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACEAAABtZW5kaXgucGxhdGZvcm0uaW52YWxpZF9jdXJyZW5jeQADVGV4dABfAQAABSRJRAAQAAAAAGjk3dCJJ6tOm4vADlABzIcCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zACMBAAAQMAADAAAAAzEAgQAAAAUkSUQAEAAAAAAmgh9B99/HWKrSbZg/RcfJAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAjAAAAVGhpcyBpcyBub3QgYSB2YWxpZCBjdXJyZW5jeSB2YWx1ZQAAAzIAkAAAAAUkSUQAEAAAAAAhQd+PEbAKV45iB4AZq6xPAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAyAAAARGUgb3BnZWdldmVuIHdhYXJkZSBpcyBnZWVuIGdlbGRpZ2UgdmFsdXRhIHdhYXJkZQAAAAAAAzMAuQEAAAUkSUQAEAAAAABKJbic/WxDSbGGXoN8cmIiAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAdAAAAbWVuZGl4LnBsYXRmb3JtLmludmFsaWRfZGF0ZQADVGV4dABKAQAABSRJRAAQAAAAAHAzj/ud5ZFOiTxEwRzGweYCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAA4BAAAQMAADAAAAAzEAdAAAAAUkSUQAEAAAAAAVhSOoRz2lV6169HCl9osfAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAWAAAAVGhpcyBpcyBubyB2YWxpZCBkYXRlAAADMgCIAAAABSRJRAAQAAAAAMWXn8mzSMNYgY4s4LRKcg8CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ACoAAABEZSBvcGdlZ2V2ZW4gd2FhcmRlIGlzIGdlZW4gZ2VsZGlnZSBkYXR1bQAAAAAAAzQAyAEAAAUkSUQAEAAAAAAdWzkhF95FRbB1BtrFGyKcAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAeAAAAbWVuZGl4LnBsYXRmb3JtLmludmFsaWRfZmxvYXQAA1RleHQAWAEAAAUkSUQAEAAAAACWpicBePEyQLBHG7+jjcSGAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAcAQAAEDAAAwAAAAMxAHwAAAAFJElEABAAAAAAnJtNoh3F8VG/0HaiFJgRJwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAHgAAAFRoaXMgdmFsdWUgc2hvdWxkIGJlIGEgbnVtYmVyAAADMgCOAAAABSRJRAAQAAAAALKcNSUYrJpSkwpC3hmrrN4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ADAAAABEZSBvcGdlZ2V2ZW4gd2FhcmRlIG1hZyBhbGxlZW4gY2lqZmVycyBiZXZhdHRlbgAAAAAAAzUA5QEAAAUkSUQAEAAAAACdGcoU7afbQIWCPv52la0PAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAgAAAAbWVuZGl4LnBsYXRmb3JtLmludmFsaWRfaW50ZWdlcgADVGV4dABzAQAABSRJRAAQAAAAAM1vpacrxT1Iqus8qX4qmV8CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zADcBAAAQMAADAAAAAzEAjgAAAAUkSUQAEAAAAADMSgoHlvnMU7xMbI2/hyq4AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAwAAAAVGhpcyB2YWx1ZSBzaG91bGQgYmUgYSBudW1iZXIgd2l0aG91dCBmcmFjdGlvbnMAAAMyAJcAAAAFJElEABAAAAAAz5sH6HBTn1KlkUlOGRPmIgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAOQAAAERlIG9wZ2VnZXZlbiB3YWFyZGUgbW9ldCBlZW4gZ2V0YWwgem9uZGVyIGRlY2ltYWxlbiB6aWpuAAAAAAADNgDgAQAABSRJRAAQAAAAAMH+oOVbTU5NkrWW8kiqV9MCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACIAAABtZW5kaXgucGxhdGZvcm0uaW50ZWdlcl9hYm92ZV9tYXgAA1RleHQAbAEAAAUkSUQAEAAAAACue9rbv+UgSoG1FNNG6AlAAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAwAQAAEDAAAwAAAAMxAIsAAAAFJElEABAAAAAAYZqsuqOTJ12hLsPA8/p3cgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALQAAAFRoaXMgdmFsdWUgc2hvdWxkIGJlIHNtYWxsZXIgdGhhbiAyMTQ3NDgzNjQ4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IHBhc3N3b3JkIHRvIGNvbnRhaW4gbm9uLWFscGhhbnVtZXJpYyBjaGFyYWN0ZXJzLiBGb3IgZXhhbXBsZTogISAjICQgb3IgJQAAAzIApgAAAAUkSUQAEAAAAAAAJ9L0LqXyUqy+Y71Bt2+1AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABIAAAASGV0IHdhY2h0d29vcmQgbW9ldCBtaW5pbWFhbCBlZW4gdmFuIGRlIHZvbGdlbmRlIHN5bWJvbGVuIGJldmF0dGVuOiB7MX0AAAAAAAMzNQDbAQAABSRJRAAQAAAAAOO0AJ4A/nxbiL1RdRmmCt4CJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACEAAABtZW5kaXgucGxhdGZvcm0ubWlzc2luZ1VwcGVyY2FzZQADVGV4dABoAQAABSRJRAAQAAAAAL+S0GVt8t1cjkLpLnMTJ0cCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zACwBAAAQMAADAAAAAzEAjgAAAAUkSUQAEAAAAAAWB13ZZXPIWaAYIv2fvRDyAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAwAAAAUGFzc3dvcmQgc2hvdWxkIGNvbnRhaW4gYW4gdXBwZXJjYXNlIGNoYXJhY3Rlci4AAAMyAIwAAAAFJElEABAAAAAAnPpHCnUfjletjIOl5c1gXwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQALgAAAEhldCB3YWNodHdvb3JkIG1vZXQgZWVuIGhvb2ZkbGV0dGVyIGJldmF0dGVuLgAAAAAAAzM2ANwBAAAFJElEABAAAAAAZ/FKCJZVHFiA/UtRGksFzwIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAIQAAAG1lbmRpeC5wbGF0Zm9ybS5taXNzaW5nTG93ZXJjYXNlAANUZXh0AGkBAAAFJElEABAAAAAAdICwwk7XTVa/jU4zi6xGbQIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMALQEAABAwAAMAAAADMQCNAAAABSRJRAAQAAAAABrukkQ7Zb5fqvpUcMCz240CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AC8AAABQYXNzd29yZCBzaG91bGQgY29udGFpbiBhIGxvd2VyY2FzZSBjaGFyYWN0ZXIuAAADMgCOAAAABSRJRAAQAAAAADeSRFe2ptJUqxTOkmLB64ECJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ADAAAABIZXQgd2FjaHR3b29yZCBtb2V0IGVlbiBrbGVpbmUgbGV0dGVyIGJldmF0dGVuLgAAAAAAAzM3ABsCAAAFJElEABAAAAAAnc8/OzHjglazl52/YYk8XQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAFgAAAG1lbmRpeC5zaWduaW4uaHR0cDQxOQADVGV4dACzAQAABSRJRAAQAAAAAMMr7t51NzZTuojrlAaGxrMCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAHcBAAAQMAADAAAAAzEAtgAAAAUkSUQAEAAAAABsEAh9PFZmWqusch4SdQ9vAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dABYAAAAWW91ciBzZXNzaW9uIGhhcyBleHBpcmVkLiBQbGVhc2UgZW50ZXIgeW91ciB1c2VyIG5hbWUgYW5kIHBhc3N3b3JkIHRvIGNvbnRpbnVlIHdvcmtpbmcuAAADMgCvAAAABSRJRAAQAAAAAP4VKyow2KNUrwynePAK4fICJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AFEAAABVdyBzZXNzaWUgaXMgdmVybG9wZW4uIFZ1bCB1dyBnZWJydWlrZXJzbmFhbSBlbiB3YWNodHdvb3JkIGluIG9tIHZlcmRlciB0ZSBnYWFuLgAAAAAAAzM4AO8BAAAFJElEABAAAAAAnid3+dzrRFSDYgGKg/BEtQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAHwAAAG1lbmRpeC5wbGF0Zm9ybS5sb25nX2Fib3ZlX21heAADVGV4dAB+AQAABSRJRAAQAAAAAMVUKFXgnv9dmlBZJ/6bu94CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAEIBAAAQMAADAAAAAzEAlAAAAAUkSUQAEAAAAABswqXD/EwUUKFdss61ioPKAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAA2AAAAVGhpcyB2YWx1ZSBzaG91bGQgYmUgc21hbGxlciB0aGFuIDkyMjMzNzIwMzY4NTQ3NzU4MDcAAAMyAJwAAAAFJElEABAAAAAA/s/WyvRlEVaLCOYw3kd8bgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAPgAAAERlIG9wZ2VnZXZlbiB3YWFyZGUgbW9ldCBrbGVpbmVyIHppam4gZGFuIDkyMjMzNzIwMzY4NTQ3NzU4MDcAAAAAAAMzOQDvAQAABSRJRAAQAAAAAIkW5MzcFLxRsyiS9nCAAwACJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB8AAABtZW5kaXgucGxhdGZvcm0ubG9uZ19iZWxvd19taW4AA1RleHQAfgEAAAUkSUQAEAAAAADCX50SORd6UaPcpyC4qrcbAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwBCAQAAEDAAAwAAAAMxAJQAAAAFJElEABAAAAAAlRIJrSXUaFuw5A3DtSlQLgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQANgAAAFRoaXMgdmFsdWUgc2hvdWxkIGJlIGxhcmdlciB0aGFuIC05MjIzMzcyMDM2ODU0Nzc1ODA4AAADMgCcAAAABSRJRAAQAAAAAGMrUPoffQxbnIJDqgwaMfcCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AD4AAABEZSBvcGdlZ2V2ZW4gd2FhcmRlIG1vZXQgZ3JvdGVyIHppam4gZGFuIC05MjIzMzcyMDM2ODU0Nzc1ODA4AAAAAAADNDAAQwIAAAUkSUQAEAAAAACXyM2Q+g3VVqz++5wD9OJiAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAlAAAAbWVuZGl4LmxpYi5WYWxpZGF0aW9ucy5yYW5nZV9kZWNpbWFsAANUZXh0AMwBAAAFJElEABAAAAAAAglhOqtg/VOKBrvWw41kNgIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAkAEAABAwAAMAAAADMQC+AAAABSRJRAAQAAAAADD5WPxm551Qqsp/7Ox5evoCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AGAAAABEZWNpbWFsIHZhbHVlcyBtYXkgY29udGFpbiBubyBtb3JlIHRoYW4gMjAgZGlnaXRzIGJlZm9yZSBhbmQgOCBkaWdpdHMgYmVoaW5kIHRoZSBkZWNpbWFsIHBvaW50LgAAAzIAwAAAAAUkSUQAEAAAAABU/bKsWoTTVa4dGxIhaMHQAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABiAAAARGVjaW1hbGUgZ2V0YWxsZW4gbW9ldGVuIG1pbmRlciBnZXRhbGxlbiBiZXZhdHRlbiBkYW4gMjAgZGVjaW1hbGVuIHZvb3IgZW4gOCBnZXRhbGxlbiBuYSBkZSBwdW50LgAAAAAAAzQxALABAAAFJElEABAAAAAA1fkE9DMxJV+y2oVMxzKWyAIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJQAAAG1lbmRpeC5saWIuVmFsaWRhdGlvbnMucmFuZ2VfaW50ZWdlcgADVGV4dAA5AQAABSRJRAAQAAAAADEY9nZOTkRVnrbh9Wa+NZsCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAP0AAAAQMAADAAAAAzEAdgAAAAUkSUQAEAAAAAAGcNdyPTxbXL8thJHvGOosAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAYAAAAVGhpcyBudW1iZXIgaXMgdG9vIGJpZy4AAAMyAHUAAAAFJElEABAAAAAArbthOLdtrF+O4EbEOmyOPAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAFwAAAERpdCBnZXRhbCBpcyB0ZSBncm9vdC4AAAAAAAM0MgCtAQAABSRJRAAQAAAAAPGAia4HIRhVnRKgM3yZK+ICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACIAAABtZW5kaXgubGliLlZhbGlkYXRpb25zLnJhbmdlX2xvbmcAA1RleHQAOQEAAAUkSUQAEAAAAABXB4zr/FWVW5xjqrEB5Yw2AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwD9AAAAEDAAAwAAAAMxAHYAAAAFJElEABAAAAAAhx05L4AkalWLYpm1AIcuQgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAGAAAAFRoaXMgbnVtYmVyIGlzIHRvbyBiaWcuAAADMgB1AAAABSRJRAAQAAAAAIDrJqZXEKpcgujv40AVxjACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ABcAAABEaXQgZ2V0YWwgaXMgdGUgZ3Jvb3QuAAAAAAADNDMAogEAAAUkSUQAEAAAAABDu+JgZ6aOXY5ZFSk9PKvRAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAbAAAAbWVuZGl4LnNpZ25pbi5nb0hvbWVCdXR0b24AA1RleHQANQEAAAUkSUQAEAAAAACe2+Jr9MbLU7/H2uhPP/76AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwD5AAAAEDAAAwAAAAMxAG8AAAAFJElEABAAAAAACYB0BFrcDli2Php2V9AsJwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAEQAAAEJhY2sgdG8gSG9tZXBhZ2UAAAMyAHgAAAAFJElEABAAAAAAou/ORNv4uFO62N0BVZQNJwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAGgAAAFRlcnVnIG5hYXIgZGUgc3RhcnRwYWdpbmEAAAAAAAM0NABAAgAABSRJRAAQAAAAALsRC9XoqF9dgJJflsIwGqICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ABYAAABtZW5kaXguc2lnbmluLmh0dHA0MDMAA1RleHQA2AEAAAUkSUQAEAAAAADBVzrQJzkGXpQNi0QcDX8rAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwCcAQAAEDAAAwAAAAMxAL8AAAAFJElEABAAAAAAwY9hXNnv5lK7NiTQHmswhQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAYQAAAFlvdSBkb24ndCBoYXZlIGVub3VnaCBwZXJtaXNzaW9ucyB0byBhY2Nlc3MgdGhpcyBwYWdlLiBZb3UgbWF5IHRyeSB0byBsb2dpbiBhcyBhIGRpZmZlcmVudCB1c2VyLgAAAzIAywAAAAUkSUQAEAAAAADbETQeIS0cXIuCNqVox7e3AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABtAAAAVSBoZWJ0IG5pZXQgZ2Vub2VnIHJlY2h0ZW4gb20gZGV6ZSBwYWdpbmEgdGUgb3BlbmVuLiBVIGt1bnQgcHJvYmVyZW4gYWFuIHRlIG1lbGRlbiBhbHMgZWVuIGFuZGVyZSBnZWJydWlrZXIuAAAAAAADNDUAdAEAAAUkSUQAEAAAAACG+uT4rH2nXLf8NO9C1504AiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQARAAAAbXh1aS5jb21tb24udHJ1ZQADVGV4dAARAQAABSRJRAAQAAAAABog+2BCW4ZBsVzGW63ybnUCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zANUAAAAQMAADAAAAAzEAYgAAAAUkSUQAEAAAAADaEXOQaz9OX4qD+Y3TaO9AAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAEAAAAWWVzAAADMgBhAAAABSRJRAAQAAAAAH0AJf3Py+FUp1rwk+GoRAICJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AAMAAABKYQAAAAAAAzQ2AHUBAAAFJElEABAAAAAAiEyl8f5SAlCPWo4ZcD1eQAIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAEgAAAG14dWkuY29tbW9uLmZhbHNlAANUZXh0ABEBAAAFJElEABAAAAAAHhXZhw+ZjUe9Pl4RPx9aWwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA1QAAABAwAAMAAAADMQBhAAAABSRJRAAQAAAAADsjgoNU0hdRl/xLj9ZN1TICJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAMAAABObwAAAzIAYgAAAAUkSUQAEAAAAAAWq8Xjd4HAU5gKBq1MsiTEAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAEAAAATmVlAAAAAAADNDcAsQEAAAUkSUQAEAAAAAAJwnKbsifOXZ7CrLTCstauAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAiAAAAbXh1aS53aWRnZXQuRGF0YUdyaWQubm9fc2VsZWN0aW9uAANUZXh0AD0BAAAFJElEABAAAAAAimhX87/Cn0aF50m91Z5YqwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAAQEAABAwAAMAAAADMQB2AAAABSRJRAAQAAAAAMQUoj+Uzb1ejxYAoHx0qa4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ABgAAABObyBzZWxlY3Rpb24gYXZhaWxhYmxlLgAAAzIAeQAAAAUkSUQAEAAAAADVbItUJcDGVZuDjRrKWspqAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAbAAAAR2VlbiBzZWxlY3RpZSBiZXNjaGlrYmFhci4AAAAAAAM0OACzAQAABSRJRAAQAAAAAHz42iNZA4lfjx+ufNAVd1YCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACQAAABteHVpLndpZGdldC5EYXRhR3JpZC5jb25maXJtX2RlbGV0ZQADVGV4dAA9AQAABSRJRAAQAAAAALrXAkIctVVAp7JxLwq5/XICJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAAEBAAAQMAADAAAAAzEAcAAAAAUkSUQAEAAAAABdMrh6EfS4X7M2VLJ3AHsxAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAASAAAARGVsZXRlIHsxfSBpdGVtcz8AAAMyAH8AAAAFJElEABAAAAAA5BX/UNxAqlK+cR0O+j9XrAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAIQAAAFdpbHQgdSB7MX0gcmVnZWwocykgdmVyd2lqZGVyZW4/AAAAAAADNDkAhgEAAAUkSUQAEAAAAABtiffUL7naXZdUfxuW95ivAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAcAAAAbXh1aS53aWRnZXQuRGF0YUdyaWQuc2VhcmNoAANUZXh0ABgBAAAFJElEABAAAAAAnaUcmd+8GkuFzxBvINqq4wIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA3AAAABAwAAMAAAADMQBlAAAABSRJRAAQAAAAAA6BB1cZMzhaveMnP98B27sCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAcAAABTZWFyY2gAAAMyAGUAAAAFJElEABAAAAAAsO5x8Myx91mvHh7st9/RPQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQABwAAAFpvZWtlbgAAAAAAAzUwAIQBAAAFJElEABAAAAAAFWKbnc6sNVCYXTC/9hCspAIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAGwAAAG14dWkud2lkZ2V0LkRhdGFHcmlkLnJlc2V0AANUZXh0ABcBAAAFJElEABAAAAAA1FZCXyaHx0qtjEp5YfYh9wIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA2wAAABAwAAMAAAADMQBkAAAABSRJRAAQAAAAAF3Qd1niOYBeqE8Y0nSHzZ4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAYAAABSZXNldAAAAzIAZQAAAAUkSUQAEAAAAADc7rtVrVxDU4lXhGAfwbN1AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAHAAAAV2lzc2VuAAAAAAADNTEA2AEAAAUkSUQAEAAAAADsr7ga/JTgWq1j2oFeezxwAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAjAAAAbXh1aS53aWRnZXQuRGF0YUdyaWQud2FpdEZvclNlYXJjaAADVGV4dABjAQAABSRJRAAQAAAAAKmrI08USOVBm4ibSF6Y20oCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zACcBAAAQMAADAAAAAzEAiAAAAAUkSUQAEAAAAAD8a6Jv4b2JU45c+6NdWgw7AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAqAAAAUGxlYXNlIGVudGVyIG9uZSBvciBtb3JlIHNlYXJjaCBjcml0ZXJpYS4AAAMyAI0AAAAFJElEABAAAAAA7W3AmvBp4FSULoy6us/H2QIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQALwAAAFZ1bCBhbHN0dWJsaWVmdCDDqcOpbiBvZiBtZWVyIHpvZWtjcml0ZXJpYSBpbi4AAAAAAAM1MgCSAQAABSRJRAAQAAAAAMA/xOGdjrxXo7eshvvKCsICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB8AAABteHVpLndpZGdldC5EaWFsb2dNZXNzYWdlLmluZm8AA1RleHQAIQEAAAUkSUQAEAAAAACdgz5Wsi/MXaMre7xvu062AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDlAAAAEDAAAwAAAAMxAGoAAAAFJElEABAAAAAAFBfa0qiDLFC8xWdTQ2wNlwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQADAAAAEluZm9ybWF0aW9uAAADMgBpAAAABSRJRAAQAAAAAJSI2W1jNb9RnduXo78TahUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AAsAAABJbmZvcm1hdGllAAAAAAADNTMAkwEAAAUkSUQAEAAAAAA/9VA3L4XqWKznX+e9cX8nAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAiAAAAbXh1aS53aWRnZXQuRGlhbG9nTWVzc2FnZS53YXJuaW5nAANUZXh0AB8BAAAFJElEABAAAAAADKw/hPBapl2MRD8Q1xSWJAIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA4wAAABAwAAMAAAADMQBmAAAABSRJRAAQAAAAAGRe0/6CuSFRiH68Bm1NgxUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAgAAABXYXJuaW5nAAADMgBrAAAABSRJRAAQAAAAAMwJEbo9eoVdnduVjp5HBpICJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AA0AAABXYWFyc2NodXdpbmcAAAAAAAM1NACHAQAABSRJRAAQAAAAAO2xSOtRtjdfphBj9D1GMh0CJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACAAAABteHVpLndpZGdldC5EaWFsb2dNZXNzYWdlLmVycm9yAANUZXh0ABUBAAAFJElEABAAAAAA0/umL1cI2F6yvao8AtMbiQIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA2QAAABAwAAMAAAADMQBkAAAABSRJRAAQAAAAAL/Iq7wg8sNYgMaGnYrGbiUCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAYAAABFcnJvcgAAAzIAYwAAAAUkSUQAEAAAAAB0+24QfjghW6YelRzobDhTAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAFAAAARm91dAAAAAAAAzU1AAQCAAAFJElEABAAAAAAy4zc6sYgAF+uaO8PvthM3wIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAKgAAAG14dWkud2lkZ2V0LkZpbGVJbnB1dC5pbmNvcnJlY3RfZXh0ZW5zaW9uAANUZXh0AIgBAAAFJElEABAAAAAA+2ddbkAZrFeB9y5fGMhOcQIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMATAEAABAwAAMAAAADMQCaAAAABSRJRAAQAAAAAD4e9d+r+zBVkfs6T9Xm6NQCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADwAAABZb3UgY2FuIG9ubHkgdXBsb2FkIGZpbGVzIG9mIHRoZSBmb2xsb3dpbmcgdHlwZXMgaGVyZTogezF9LgAAAzIAoAAAAAUkSUQAEAAAAABP2no4vhRGVoMGd8bZx6MlAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABCAAAAVSBrdW50IGhpZXIgYWxsZWVuIGJlc3RhbmRlbiB2YW4gZGUgdm9sZ2VuZGUgdHlwZXMgdXBsb2FkZW46IHsxfS4AAAAAAAM1NgB/AQAABSRJRAAQAAAAAAGlGvgDiZRfsV+GhhzF0HcCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB0AAABteHVpLndpZGdldC5EaWFsb2dNZXNzYWdlLm9rAANUZXh0ABABAAAFJElEABAAAAAA5Fb1bu2Ai1mc9DMXeKrGBQIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA1AAAABAwAAMAAAADMQBhAAAABSRJRAAQAAAAANIwWyD0E1lYhmJRx/IjadACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAMAAABPSwAAAzIAYQAAAAUkSUQAEAAAAAAIB4bfNMLkXLpEnBL0CA79AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAADAAAAT0sAAAAAAAM1NwCWAQAABSRJRAAQAAAAAFo2OaZ8hT1bpdohRAJ8PswCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB8AAABteHVpLndpZGdldC5MaXN0Vmlldy5sb2FkX21vcmUAA1RleHQAJQEAAAUkSUQAEAAAAAA94hIrLQirV4w0I0zxhz+JAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDpAAAAEDAAAwAAAAMxAGsAAAAFJElEABAAAAAAp721BQHFL1q36hnAs3zYhAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQADQAAAExvYWQgbW9yZS4uLgAAAzIAbAAAAAUkSUQAEAAAAADG01t+ywONXIC7/P59iPPzAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAOAAAATWVlciBsYWRlbi4uLgAAAAAAAzU4AIYBAAAFJElEABAAAAAAxlygr74PXVSU01b658xFTQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAHAAAAG14dWkud2lkZ2V0Lkxpc3RWaWV3LnNlYXJjaAADVGV4dAAYAQAABSRJRAAQAAAAAPVMl2/YxNFdhi5gMDcVItkCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zANwAAAAQMAADAAAAAzEAZQAAAAUkSUQAEAAAAABG0xTrFQ0NXIMgaKmf4QxrAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAHAAAAU2VhcmNoAAADMgBlAAAABSRJRAAQAAAAAOuLul72XcNUuNo7p7SJeP8CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AAcAAABab2VrZW4AAAAAAAM1OQCiAQAABSRJRAAQAAAAAHLxw/Mca0hauOpqLcsAewACJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB4AAABteHVpLndpZGdldC5MaXN0Vmlldy5ub19pdGVtcwADVGV4dAAyAQAABSRJRAAQAAAAAB1Da3bAfOJQqQ/O2QZSBBMCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAPYAAAAQMAADAAAAAzEAbQAAAAUkSUQAEAAAAAAdxC5Tk4hOU5kRUTu+l1ojAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAPAAAATm8gaXRlbXMgZm91bmQAAAMyAHcAAAAFJElEABAAAAAAoT6qm/gouVWxHhU951XP8QIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAGQAAAEdlZW4gcmVzdWx0YXRlbiBnZXZvbmRlbgAAAAAAAzYwAI8BAAAFJElEABAAAAAAz2hhgKXoSlO1qhqjtWCyOwIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAHwAAAG14dWkud2lkZ2V0LkZpbGVJbnB1dC5kb3dubG9hZAADVGV4dAAeAQAABSRJRAAQAAAAAMZo6DfFMmBSs9jqu7gK90MCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAOIAAAAQMAADAAAAAzEAZwAAAAUkSUQAEAAAAAAhxD/q0KrgW698qv6dKdwxAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAJAAAARG93bmxvYWQAAAMyAGkAAAAFJElEABAAAAAA1W9eKDS0aF+kAL8N7GnbdgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQACwAAAERvd25sb2FkZW4AAAAAAAM2MQCPAQAABSRJRAAQAAAAADE9FdH0NudYmRvHZR+IHlMCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5AB0AAABteHVpLndpZGdldC5GaWxlSW5wdXQudXBsb2FkAANUZXh0ACABAAAFJElEABAAAAAA87bKCye9AFe48Xwz5X5EyAIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA5AAAABAwAAMAAAADMQBoAAAABSRJRAAQAAAAAJnluhnofxZbtuar1cVKsK4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAoAAABCcm93c2UuLi4AAAMyAGoAAAAFJElEABAAAAAAP4Dcrth1oFyXYzbkdY4oxgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQADAAAAEJsYWRlcmVuLi4uAAAAAAADNjIA8wEAAAUkSUQAEAAAAACt2XhHb6gxW4HaPw/iPWQOAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAdAAAAbXh1aS5zeXMuVUkuY29ubmVjdGlvbl9lcnJvcgADVGV4dACEAQAABSRJRAAQAAAAACxN8lC2taFYq+mP4wVBZlQCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAEgBAAAQMAADAAAAAzEAkwAAAAUkSUQAEAAAAAAqTq/DzAwiXYVluVYxzwIEAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAA1AAAAQSBjb25uZWN0aW9uIGVycm9yIG9jY3VycmVkLCBwbGVhc2UgdHJ5IGFnYWluIGxhdGVyLgAAAzIAowAAAAUkSUQAEAAAAADRmCHafvF+W5/9domPtoSRAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABFAAAARXIgaXMgZWVuIHZlcmJpbmRpbmdzZm91dCBvcGdldHJlZGVuLCBwcm9iZWVydCB1IGhldCBsYXRlciBub2cgZWVucy4AAAAAAAM2MwDtAQAABSRJRAAQAAAAAAdZ8F58mzZaqBNLfmmUC4gCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACYAAABteHVpLndpZGdldC5EYXRhVmlldy52YWxpZGF0aW9uX2Vycm9yAANUZXh0AHUBAAAFJElEABAAAAAAz4Y/roek4V6BK61yjBMcngIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAOQEAABAwAAMAAAADMQCRAAAABSRJRAAQAAAAAHYb2nURDupWrazkSR1Sr+kCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADMAAABPbmUgb3IgbW9yZSBpbnB1dCBmaWVsZHMgY29udGFpbiBhbiBpbnZhbGlkIHZhbHVlLgAAAzIAlgAAAAUkSUQAEAAAAADbbNAB6Sd0XYms70meL+NKAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAA4AAAARWVuIG9mIG1lZXIgaW52b2VydmVsZGVuIGJldmF0dGVuIGVlbiBvbmdlbGRpZ2Ugd2FhcmRlLgAAAAAAAzY0ACsCAAAFJElEABAAAAAASljbE0hn41O/pLfiVCxbHQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJQAAAG14dWkud2lkZ2V0LkZpbGVJbnB1dC5maWxlX3Rvb19sYXJnZQADVGV4dAC0AQAABSRJRAAQAAAAAMQtR8wtrPVUgTTupNYzpAYCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAHgBAAAQMAADAAAAAzEArQAAAAUkSUQAEAAAAADGvptoh/ijVbrFmkrvDeKGAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dABPAAAAVGhlIHNpemUgb2YgdGhlIHNlbGVjdGVkIGZpbGUgKHsxfU1CKSBpcyBsYXJnZXIgdGhhbiB0aGUgbWF4aW11bSBzaXplICh7Mn1NQikuAAADMgC5AAAABSRJRAAQAAAAAE/IvsbmYWFUgx24TAjkInoCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AFsAAABEZSBncm9vdHRlIHZhbiBoZXQgZ2VzZWxlY3RlZXJkZSBiZXN0YW5kICh7MX1NQikgaXMgaG9nZXIgZGFuIGRlIG1heGltYWxlIGdyb290dGUgKHsyfU1CKS4AAAAAAAM2NQDvAQAABSRJRAAQAAAAABSMMeAWm2JSivpemzfD6ScCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ADEAAABteHVpLndpZGdldC5SZWZlcmVuY2VTZWxlY3Rvci5jbGVhbl9jb25maXJtYXRpb24AA1RleHQAbAEAAAUkSUQAEAAAAACAcPMVT4rmW6SGvEIoJORrAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwAwAQAAEDAAAwAAAAMxAI0AAAAFJElEABAAAAAAjq9+2qxTr1mO8W35TcSfkwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQALwAAAEFyZSB5b3Ugc3VyZSB5b3Ugd2FudCB0byBjbGVhciB0aGlzIHJlZmVyZW5jZT8AAAMyAJEAAAAFJElEABAAAAAALa/ZJOOmFVmyXb0e1OxNewIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAMwAAAFdlZXQgdSB6ZWtlciBkYXQgdSBkZXplIHZlcndpanppbmcgd2lsdCBsZWVnbWFrZW4/AAAAAAADNjYA7gEAAAUkSUQAEAAAAAAqRPzl2CxcWrTcyz2zinBlAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAqAAAAbXh1aS53aWRnZXQuVGFiQ29udGVudC5oaWRkZW5fdmFsaWRhdGlvbnMAA1RleHQAcgEAAAUkSUQAEAAAAABhzssWh3lIW7vc1z97KoCMAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwA2AQAAEDAAAwAAAAMxAJMAAAAFJElEABAAAAAA+WnUnwr6O1W4Qh9MspIHSwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQANQAAAFRhYiB7MX0gY29udGFpbnMgdmFsaWRhdGlvbiBlcnJvcnMgYnV0IGlzIGludmlzaWJsZS4AAAMyAJEAAAAFJElEABAAAAAANlUf/7gtBFWTVe7N3T0DSAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAMwAAAFRhYiB7MX0gYmV2YXQgdmFsaWRhdGllZm91dGVuIG1hYXIgaXMgb256aWNodGJhYXIuAAAAAAADNjcA+AEAAAUkSUQAEAAAAABp08JOBsuTUJs7mCWEXcpRAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAbAAAAbXh1aS5zeXMuVUkuaW50ZXJuYWxfZXJyb3IAA1RleHQAiwEAAAUkSUQAEAAAAABYAgw9OSsKXL2wBHVp54rbAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwBPAQAAEDAAAwAAAAMxAJsAAAAFJElEABAAAAAAUPd9l8wi71+xKV6qaXgaxwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAPQAAAEFuIGVycm9yIG9jY3VycmVkLCBwbGVhc2UgY29udGFjdCB5b3VyIHN5c3RlbSBhZG1pbmlzdHJhdG9yLgAAAzIAogAAAAUkSUQAEAAAAADtmc/N4/RBUIY6JWLIOSYTAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dABEAAAARXIgaXMgZWVuIGZvdXQgb3BnZXRyZWRlbiwgbmVlbSBjb250YWN0IG9wIG1ldCB1dyBzeXN0ZWVtYmVoZWVyZGVyLgAAAAAAAzY4AKoBAAAFJElEABAAAAAA9Xk/sygm6F+S6g65dRVQ5AIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJAAAAG14dWkud2lkZ2V0LlNlbGVjdEJveC5pdGVtc1NlbGVjdGVkAANUZXh0ADQBAAAFJElEABAAAAAADWXX86MjY1am22JZWrfOSwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA+AAAABAwAAMAAAADMQBxAAAABSRJRAAQAAAAAJU5/ArQJ4hdsqbJdlkqa4oCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ABMAAAB7MX0gaXRlbXMgc2VsZWN0ZWQAAAMyAHUAAAAFJElEABAAAAAARWiCI42jAV2Xy0ecLZkphAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAFwAAAHsxfSBpdGVtcyBnZXNlbGVjdGVlcmQAAAAAAAM2OQAzAgAABSRJRAAQAAAAACekFWiBRKZTmQ3XAYXuDIECJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ABcAAABteHVpLnN5cy5VSS5zeW5jX2Vycm9yAANUZXh0AMoBAAAFJElEABAAAAAAbky91LSbwFmmdLGRFwAIbQIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAjgEAABAwAAMAAAADMQCzAAAABSRJRAAQAAAAABtanoZ5NgZQoshMZcmckFACJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AFUAAABZb3VyIGNoYW5nZXMgY2Fubm90IGJlIHN5bmNocm9uaXplZCBhdCB0aGlzIG1vbWVudC4gVGhleSB3aWxsIGJlIHN5bmNocm9uaXplZCBsYXRlci4AAAMyAMkAAAAFJElEABAAAAAAGoeY7k0AT1m5Gij3xOdOmwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAawAAAFV3IHdpanppZ2luZ2VuIGt1bm5lbiBuaWV0IHdvcmRlbiBnZXN5bmNocm9uaXNlZXJkIG9wIGRpdCBtb21lbnQuIFplIHp1bGxlbiBsYXRlciB3b3JkZW4gZ2VzeW5jaHJvbmlzZWVyZC4AAAAAAAM3MACCAQAABSRJRAAQAAAAAMrcOYvD8RxejCI0OI89BzICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ABUAAABtZW5kaXguc2lnbmluLmNhbmNlbAADVGV4dAAbAQAABSRJRAAQAAAAADiNoIZFVVRQlF0J2j3EpvACJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAN8AAAAQMAADAAAAAzEAaAAAAAUkSUQAEAAAAABwO2Z0Ioo8XZjRPAQP1AZeAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAKAAAAQW5udWxlcmVuAAADMgBlAAAABSRJRAAQAAAAAIfbQc6njzteh+R6/LohXRQCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAcAAABDYW5jZWwAAAAAAAM3MQClAQAABSRJRAAQAAAAAA/ZOMFi111Ym2/PDzATXkICJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACcAAABteHVpLndpZGdldC5EYXRlUGlja2VyLm9wZW5fZGF0ZXBpY2tlcgADVGV4dAAsAQAABSRJRAAQAAAAAKNqv1zWkmdQucVVHg3GeOgCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAPAAAAAQMAADAAAAAzEAbwAAAAUkSUQAEAAAAAAyxJTL42okVZyQ/xUt87YPAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAARAAAAVG9vbiBkYXR1bWtpZXplcgAAAzIAbwAAAAUkSUQAEAAAAADBGWaKm8C+UI9an+6aRmXMAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAARAAAAU2hvdyBkYXRlIHBpY2tlcgAAAAAAAzcyAI4BAAAFJElEABAAAAAA67f/oWHKeFGj/wvgGqGEuQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAIQAAAG14dWkud2lkZ2V0LkRpYWxvZ01lc3NhZ2UuY2FuY2VsAANUZXh0ABsBAAAFJElEABAAAAAAo0nWjUmlBVq+YdbO6sB4IwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA3wAAABAwAAMAAAADMQBoAAAABSRJRAAQAAAAAFu/7nuntB1VpX/WMpS027sCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AAoAAABBbm51bGVyZW4AAAMyAGUAAAAFJElEABAAAAAAeur4jm4TV1ul1JucL839AAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQABwAAAENhbmNlbAAAAAAAAzczAKMBAAAFJElEABAAAAAA3sp1L3p+3Fyq0hHPEDOI1wIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAKAAAAG14dWkud2lkZ2V0LkRpYWxvZ01lc3NhZ2UuZmlsZV9kb3dubG9hZAADVGV4dAApAQAABSRJRAAQAAAAANrnJ31S8ahcjbaGQ0Onyh0CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAO0AAAAQMAADAAAAAzEAbwAAAAUkSUQAEAAAAADNDMuxXq0VVaEPShQmJgbFAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAARAAAARG93bmxvYWQgYmVzdGFuZAAAAzIAbAAAAAUkSUQAEAAAAABxJtia5LVfXrzw/vAhHuMaAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAOAAAARG93bmxvYWQgZmlsZQAAAAAAAzc0AOkBAAAFJElEABAAAAAA4sJNdjNAXlCpWAth3jEb2QIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAMAAAAG14dWkud2lkZ2V0LkRpYWxvZ01lc3NhZ2UuZmlsZV9kb3dubG9hZF9tZXNzYWdlAANUZXh0AGcBAAAFJElEABAAAAAAld4U5iqXCFCdPk5s0HgalgIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAKwEAABAwAAMAAAADMQCOAAAABSRJRAAQAAAAAPJKMKjVObtfnf05t/VDakMCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ADAAAABXZWV0IHUgemVrZXIgZGF0IHUgZGl0IGJlc3RhbmQgd2lsdCBkb3dubG9hZGVuPwAAAzIAiwAAAAUkSUQAEAAAAAAl3Lm+h5aQXKDj8YmT7P3MAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAtAAAAQXJlIHlvdSBzdXJlIHlvdSB3YW50IHRvIGRvd25sb2FkIHRoaXMgZmlsZT8AAAAAAAM3NQCcAQAABSRJRAAQAAAAAKoFeea4cXpdox5tmY5MUGACJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACcAAABteHVpLndpZGdldC5Db25maXJtYXRpb25EaWFsb2cuY2FwdGlvbgADVGV4dAAjAQAABSRJRAAQAAAAALRDcsY6wd9QtG+jF5uwiPwCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAOcAAAAQMAADAAAAAzEAagAAAAUkSUQAEAAAAAB7/cHa6s07UKkGrTjvYd7wAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAMAAAAQmV2ZXN0aWdpbmcAAAMyAGsAAAAFJElEABAAAAAAfto57rGkLFaiwF155hcioAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQADQAAAENvbmZpcm1hdGlvbgAAAAAAAzc2AMYBAAAFJElEABAAAAAAROiSuBvMN1aNs0+rfg0STgIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAGwAAAG14dWkuc3lzLlVJLmNvbmZpcm1fZGVsZXRlAANUZXh0AFkBAAAFJElEABAAAAAAQacRYWg7TVSXqstnATlX0QIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAHQEAABAwAAMAAAADMQCHAAAABSRJRAAQAAAAACmY1Vtyi4dQtmiaiGPdy/oCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ACkAAABXZWV0IHUgemVrZXIgZGF0IHUgZGl0IHdpbHQgdmVyd2lqZGVyZW4/AAADMgCEAAAABSRJRAAQAAAAAEKfrji54V5Qj72ibFwcHl4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ACYAAABBcmUgeW91IHN1cmUgeW91IHdhbnQgdG8gZGVsZXRlIHRoaXM/AAAAAAADNzcAtgEAAAUkSUQAEAAAAACdj3fClPhxUKVxFvDTz1H/AiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAlAAAAbWVuZGl4LmxpYi5WYWxpZGF0aW9ucy5pbnZhbGlkX3ZhbHVlAANUZXh0AD8BAAAFJElEABAAAAAAJUgfDgiib1iL37zpXlL/OAIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMAAwEAABAwAAMAAAADMQB6AAAABSRJRAAQAAAAACjc/9TOolpfim/juxIUhCwCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ABwAAABEZXplIHdhYXJkZSBpcyBuaWV0IGdlbGRpZy4AAAMyAHcAAAAFJElEABAAAAAAONoh28zi7l6iAnpdmn0STwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAGQAAAFRoaXMgdmFsdWUgaXMgbm90IHZhbGlkLgAAAAAAAzc4AJ8BAAAFJElEABAAAAAAtfKtCIcetVm9naxwx13z+AIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkALAAAAG1lbmRpeC5saWIuTXhEYXRhU291cmNlLnN0YXR1c193aXRob3V0X3NpemUAA1RleHQAIQEAAAUkSUQAEAAAAADbPplOIRQKXLTC/bM8j7hEAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDlAAAAEDAAAwAAAAMxAGoAAAAFJElEABAAAAAAxRpk3bQPw1+V6xd7OYRvZwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQADAAAAHsxfSB0b3QgezJ9AAADMgBpAAAABSRJRAAQAAAAAMoMfJhizc5flbA7T/oJcD0CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAsAAAB7MX0gdG8gezJ9AAAAAAADNzkAmAEAAAUkSUQAEAAAAADVlzEJMrp3WLyZNXPBe1QwAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAtAAAAbXh1aS53aWRnZXQuU2VhcmNoSW5wdXQuZW1wdHlfZHJvcGRvd25fdmFsdWUAA1RleHQAGQEAAAUkSUQAEAAAAAB5Bx2oa6yxXaZo/R//f7RPAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDdAAAAEDAAAwAAAAMxAGUAAAAFJElEABAAAAAACKM/HwZPVFisrmnpD5NmFQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQABwAAAChsZWVnKQAAAzIAZgAAAAUkSUQAEAAAAABj/PXsF4fIXIHQZKb+ocT+AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAIAAAAKGVtcHR5KQAAAAAAAzgwAIwBAAAFJElEABAAAAAAg9hX50N5EVq8bHOc4otAVQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJQAAAG14dWkud2lkZ2V0Lm5hdGl2ZS5Nb2RhbERpYWxvZy5jbG9zZQADVGV4dAAVAQAABSRJRAAQAAAAANw2LRZJsllaodXrV/IPPGkCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zANkAAAAQMAADAAAAAzEAZAAAAAUkSUQAEAAAAAADI57rq1BIUqipz+HgjvnqAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAGAAAAS2xhYXIAAAMyAGMAAAAFJElEABAAAAAAeTdjtRVijlCCbV4AFsyJgAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQABQAAAERvbmUAAAAAAAM4MQCSAQAABSRJRAAQAAAAAJE8bsiMhD5eoGrSWivhwswCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACgAAABteHVpLndpZGdldC5Ecm9wRG93bkJ1dHRvbi5jbG9zZV9idXR0b24AA1RleHQAGAEAAAUkSUQAEAAAAAC/CH4PhEjcWZjND2sXrI1BAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDcAAAAEDAAAwAAAAMxAGYAAAAFJElEABAAAAAA8rse7bTwOFGLSwn8jjJvKwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQACAAAAFNsdWl0ZW4AAAMyAGQAAAAFJElEABAAAAAAxmZ0OQtRF1GCm3BxkfxF4wIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQABgAAAENsb3NlAAAAAAADODIApwEAAAUkSUQAEAAAAADBFeviGIEqXIuFWeKVr4kUAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAhAAAAbXh1aS53aWRnZXQuR3JpZC5hMTF5LmZpcnN0X3BhZ2UAA1RleHQANAEAAAUkSUQAEAAAAAB+HjLgH5K5UIPHHug5K3X0AiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwD4AAAAEDAAAwAAAAMxAHcAAAAFJElEABAAAAAAYnaXYxjprlmY9GOfloDm8gIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQAGQAAAEdhIG5hYXIgZGUgZWVyc3RlIHBhZ2luYQAAAzIAbwAAAAUkSUQAEAAAAACFfoTi44DYX7N4IXLu6VMrAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAARAAAAR28gdG8gZmlyc3QgcGFnZQAAAAAAAzgzAK0BAAAFJElEABAAAAAAeX5jXzDBAVu/HZ9nu3+10QIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJAAAAG14dWkud2lkZ2V0LkdyaWQuYTExeS5wcmV2aW91c19wYWdlAANUZXh0ADcBAAAFJElEABAAAAAA8cFIQLMvEFqVKJSvYf6zuAIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA+wAAABAwAAMAAAADMQB3AAAABSRJRAAQAAAAAEOKLCO/t7VSm1GUANgIpHQCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ABkAAABHYSBuYWFyIGRlIHZvcmlnZSBwYWdpbmEAAAMyAHIAAAAFJElEABAAAAAAGqIfT6vhiFiQC/aT0vH03wIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQAFAAAAEdvIHRvIHByZXZpb3VzIHBhZ2UAAAAAAAM4NACnAQAABSRJRAAQAAAAAOIDOrYTf31fjdCrOFmuEN4CJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACAAAABteHVpLndpZGdldC5HcmlkLmExMXkubmV4dF9wYWdlAANUZXh0ADUBAAAFJElEABAAAAAA1ALRZo0tV1O9kQYiTcoXGwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA+QAAABAwAAMAAAADMQB5AAAABSRJRAAQAAAAAMnlh2/YYOdcpd/5k10faOECJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ABsAAABHYSBuYWFyIGRlIHZvbGdlbmRlIHBhZ2luYQAAAzIAbgAAAAUkSUQAEAAAAADPEsiHGdLeUrYgS56Vx4x8AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAQAAAAR28gdG8gbmV4dCBwYWdlAAAAAAADODUApgEAAAUkSUQAEAAAAABxiNnD3FDfVZw/wrga2+RYAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAgAAAAbXh1aS53aWRnZXQuR3JpZC5hMTF5Lmxhc3RfcGFnZQADVGV4dAA0AQAABSRJRAAQAAAAAO5wqmlL1HVUqhHno2FLXXUCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAPgAAAAQMAADAAAAAzEAeAAAAAUkSUQAEAAAAACBBfhliByvUo1lXfHJIIUJAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAaAAAAR2EgbmFhciBkZSBsYWF0c3RlIHBhZ2luYQAAAzIAbgAAAAUkSUQAEAAAAACSY9funTsHVqeYm+qoPUx3AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAQAAAAR28gdG8gbGFzdCBwYWdlAAAAAAADODYAoQEAAAUkSUQAEAAAAAA/PXb9jkNTX67jqSYkHnd2AiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAiAAAAbXh1aS53aWRnZXQuR3JpZC5hMTF5LnBhZ2Vfc3RhdHVzAANUZXh0AC0BAAAFJElEABAAAAAA627Rb1GWgFKdmTirLVlnNAIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA8QAAABAwAAMAAAADMQBvAAAABSRJRAAQAAAAABCi7Vhy4l5YjehG9cz0PhcCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0ABEAAABIdWlkaWdlIHdlZXJnYXZlAAADMgBwAAAABSRJRAAQAAAAAJ7qoO7GRshXrRcuH5meW3UCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ABIAAABDdXJyZW50bHkgc2hvd2luZwAAAAAAAzg3AJ4BAAAFJElEABAAAAAAj5r+cLvwhV2StsdOipHGUgIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkALAAAAG14dWkud2lkZ2V0LkdyaWQuYTExeS5uYXZpZ2F0aW9uX2FyaWFfbGFiZWwAA1RleHQAIAEAAAUkSUQAEAAAAAD2Xyz6xKwrVLF49emxUcvdAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDkAAAAEDAAAwAAAAMxAGkAAAAFJElEABAAAAAAq+wkDQSo/luOt5RyEO9rBQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQACwAAAFBhZ2luZXJpbmcAAAMyAGkAAAAFJElEABAAAAAAbxvyc3VFcVeas7zTBkcNFAIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQACwAAAFBhZ2luYXRpb24AAAAAAAM4OACKAQAABSRJRAAQAAAAADv0NIlbwmlciC0Wuy6iu1gCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ACMAAABteHVpLndpZGdldC5uYXRpdmUuTmF2aWdhdGlvbi5iYWNrAANUZXh0ABUBAAAFJElEABAAAAAAjU1LlFzBllq/km94fIM3RgIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA2QAAABAwAAMAAAADMQBkAAAABSRJRAAQAAAAAJYq4fo4sBdesySH/agRkosCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AAYAAABUZXJ1ZwAAAzIAYwAAAAUkSUQAEAAAAADS+IFM4SwLVauqdvIyDtKUAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAFAAAAQmFjawAAAAAAAzg5AP0BAAAFJElEABAAAAAA1z4+9bXSE1uLurUc0gr/ZgIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAOwAAAG14dWkud2lkZ2V0LlJlZmVyZW5jZVNldFNlbGVjdG9yLmNvbmZpcm1fcmVtb3ZlX3JlZmVyZW5jZXMAA1RleHQAcAEAAAUkSUQAEAAAAACcA8YudjKaVpjrXunwiBUzAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwA0AQAAEDAAAwAAAAMxAJQAAAAFJElEABAAAAAAvHFb0+hCTV2qRsPrgCFWAgIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQANgAAAFdlZXQgdSB6ZWtlciBkYXQgdSB7MX0gdmVyd2lqemluZ2VuIHdpbHQgdmVyd2lqZGVyZW4/AAADMgCOAAAABSRJRAAQAAAAANASrazgoTFSi6Kc6OxvjyICJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0ADAAAABBcmUgeW91IHN1cmUgeW91IHdhbnQgdG8gcmVtb3ZlIHsxfSByZWZlcmVuY2VzPwAAAAAAAzkwAPsBAAAFJElEABAAAAAAOBF0hlpOZFOZZyJk1TVBLwIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAOgAAAG14dWkud2lkZ2V0LlJlZmVyZW5jZVNldFNlbGVjdG9yLmNvbmZpcm1fcmVtb3ZlX3JlZmVyZW5jZQADVGV4dABvAQAABSRJRAAQAAAAAOaIHgM/1apZi8LlFe1SWt8CJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zADMBAAAQMAADAAAAAzEAkwAAAAUkSUQAEAAAAABpYMNgMUsjV7KTlI47MsBfAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAA1AAAAV2VldCB1IHpla2VyIGRhdCB1IGRlemUgdmVyd2lqemluZyB3aWx0IHZlcndpamRlcmVuPwAAAzIAjgAAAAUkSUQAEAAAAABSB8UDdDL8WYV80RAqubUsAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAwAAAAQXJlIHlvdSBzdXJlIHlvdSB3YW50IHRvIHJlbW92ZSB0aGlzIHJlZmVyZW5jZT8AAAAAAAM5MQCDAQAABSRJRAAQAAAAADAjlY//OUFXkV/UkqWTTEoCJFR5cGUAEQAAAFRleHRzJFN5c3RlbVRleHQAAkludGVybmFsS2V5ABkAAABteHVpLndpZGdldC5XaW5kb3cuY2xvc2UAA1RleHQAGAEAAAUkSUQAEAAAAAAG1weTkmQoUrruyvei7JtrAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDcAAAAEDAAAwAAAAMxAGYAAAAFJElEABAAAAAAXMWg53Uy/lqZIJsh7kqNVQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQACAAAAHNsdWl0ZW4AAAMyAGQAAAAFJElEABAAAAAAPMhMeMpkCVmMhXzKf0eHHQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQABgAAAGNsb3NlAAAAAAADOTIAiwEAAAUkSUQAEAAAAAAOYiD7U4ZGXpas0mg0nQ+BAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAnAAAAbXh1aS53aWRnZXQuRmlsZUlucHV0Lm5vX2ZpbGVfc2VsZWN0ZWQAA1RleHQAEgEAAAUkSUQAEAAAAABK0xaMcI+3W7AoyNWY35kGAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDWAAAAEDAAAwAAAAMxAGIAAAAFJElEABAAAAAAbyayMhHDzlmO8WKyuVSFuQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQABAAAAC4uLgAAAzIAYgAAAAUkSUQAEAAAAAAk76al2mUAU5OViavH+VyiAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAAEAAAALi4uAAAAAAADOTMAkQEAAAUkSUQAEAAAAAAe0D3j8AbkW5LbSC3r6cJyAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAhAAAAbXh1aS53aWRnZXQuRGF0YUdyaWQucGFnZV9udW1iZXIAA1RleHQAHgEAAAUkSUQAEAAAAAA+mL9/g/1PX4+kimz7DC9xAiRUeXBlAAsAAABUZXh0cyRUZXh0AARJdGVtcwDiAAAAEDAAAwAAAAMxAGkAAAAFJElEABAAAAAAY6rIF485a1CHstLdDsGRtQIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAbmxfTkwAAlRleHQACwAAAFBhZ2luYSB7MX0AAAMyAGcAAAAFJElEABAAAAAA+RSIwZ2uvFaB7j+cB1o5agIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQACQAAAFBhZ2UgezF9AAAAAAADOTQAngEAAAUkSUQAEAAAAADiK+KUO+ZzXbrMyBxMw7IrAiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQApAAAAbXh1aS53aWRnZXQubmF0aXZlLk5hdmlnYXRpb24ucmVzdGFydEFwcAADVGV4dAAjAQAABSRJRAAQAAAAADJr/GmM5SZWhdaaGp7x04MCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAOcAAAAQMAADAAAAAzEAawAAAAUkSUQAEAAAAABro3YOypFVVp56l4SoQZE6AiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAANAAAASGVyc3RhcnQgYXBwAAADMgBqAAAABSRJRAAQAAAAAN7jcRn8PRZRgvz46xhzXq4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAwAAABSZXN0YXJ0IGFwcAAAAAAAAzk1AJcBAAAFJElEABAAAAAAKUj5mOYhl1iZQ72hlwa7XAIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJwAAAG14dWkud2lkZ2V0Lm5hdGl2ZS5OYXZpZ2F0aW9uLmNsb3NlQXBwAANUZXh0AB4BAAAFJElEABAAAAAAzQg5A0XXqV6g1xlLdIRJVwIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA4gAAABAwAAMAAAADMQBoAAAABSRJRAAQAAAAADIBSZGJEX5dovxT54Fujj4CJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AAoAAABTbHVpdCBhcHAAAAMyAGgAAAAFJElEABAAAAAAhV3ZGJPLelaSWAmSmdMoHwIkVHlwZQASAAAAVGV4dHMkVHJhbnNsYXRpb24AAkxhbmd1YWdlQ29kZQAGAAAAZW5fVVMAAlRleHQACgAAAENsb3NlIGFwcAAAAAAAAzk2AJICAAAFJElEABAAAAAAfO8Nu/zO6FGgRDKj/2kevQIkVHlwZQARAAAAVGV4dHMkU3lzdGVtVGV4dAACSW50ZXJuYWxLZXkAJAAAAG14dWkuc3lzLlVJLnNpbXVsdGFuZW91c19zeW5jX2Vycm9yAANUZXh0ABwCAAAFJElEABAAAAAA3/IDhXS+0l6S6rz+bWDAXgIkVHlwZQALAAAAVGV4dHMkVGV4dAAESXRlbXMA4AEAABAwAAMAAAADMQD0AAAABSRJRAAQAAAAADx1dX+DTSpXk7VQObHAFlQCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAG5sX05MAAJUZXh0AJYAAABIZXQgdWl0dm9lcmVuIHZhbiBnZWxpamt0aWpkaWdlIHN5bmNocm9uaXNhdGllcyB3b3JkdCBuaWV0IG9uZGVyc3RldW5kLiBQcm9iZWVyIGhldCBhbHN0dWJsaWVmdCBub2dtYWFscyBuYWRhdCBkZSBodWlkaWdlIHN5bmNocm9uaXNhdGllIGlzIHZvbHRvb2lkLgAAAzIA2gAAAAUkSUQAEAAAAACmk7zkak7IXbVDpqfC3zriAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABlbl9VUwACVGV4dAB8AAAAUGVyZm9ybWluZyBzaW11bHRhbmVvdXMgc3luY2hyb25pemF0aW9ucyBpcyBub3Qgc3VwcG9ydGVkLiBQbGVhc2UgdHJ5IGFnYWluIGFmdGVyIHRoZSBjdXJyZW50IHN5bmNocm9uaXphdGlvbiBpcyBjb21wbGV0ZWQuAAAAAAADOTcAiAEAAAUkSUQAEAAAAAC1JHC7p7+RUK9aTZGa3t74AiRUeXBlABEAAABUZXh0cyRTeXN0ZW1UZXh0AAJJbnRlcm5hbEtleQAaAAAAbWVuZGl4LnNpZ25pbi5sb2dpbkhlYWRlcgADVGV4dAAcAQAABSRJRAAQAAAAAM3Zk7+WMPdQq1eItndP39MCJFR5cGUACwAAAFRleHRzJFRleHQABEl0ZW1zAOAAAAAQMAADAAAAAzEAaAAAAAUkSUQAEAAAAAB0ZB5VQvhhWKWLp8tUdvabAiRUeXBlABIAAABUZXh0cyRUcmFuc2xhdGlvbgACTGFuZ3VhZ2VDb2RlAAYAAABubF9OTAACVGV4dAAKAAAAQWFubWVsZGVuAAADMgBmAAAABSRJRAAQAAAAAFmgxx5gQgtZlnIXl9nkVkoCJFR5cGUAEgAAAFRleHRzJFRyYW5zbGF0aW9uAAJMYW5ndWFnZUNvZGUABgAAAGVuX1VTAAJUZXh0AAgAAABTaWduIGluAAAAAAAAAA==" + }, + { + "unit_id": "bd6f65bd-2393-539d-9388-4684a54d0646", + "container_id": "82d1d7d0-ca49-4703-8557-1f478ee46d7c", + "module": null, + "containment": "ProjectDocuments", + "name": "", + "type": "Security$ProjectSecurity", + "contents": "9AQAAAUkSUQAEAAAAAC9ZW+9kyOdU5OIRoSlTQZGAiRUeXBlABkAAABTZWN1cml0eSRQcm9qZWN0U2VjdXJpdHkAAkFkbWluUGFzc3dvcmQACgAAAE1lbmRpeDEyMwACQWRtaW5Vc2VyTmFtZQAIAAAATXhBZG1pbgACQWRtaW5Vc2VyUm9sZQABAAAAAAhDaGVja1NlY3VyaXR5AAEERGVtb1VzZXJzAD0CAAAQMAACAAAAAzEAyQAAAAUkSUQAEAAAAAAPxCAGGcsAQJQZWIEJOQ/uAiRUeXBlABYAAABTZWN1cml0eSREZW1vVXNlckltcGwAAkVudGl0eQAXAAAAQWRtaW5pc3RyYXRpb24uQWNjb3VudAACUGFzc3dvcmQACwAAAEtPNWtPcmVZa0kAAlVzZXJOYW1lABMAAABkZW1vX2FkbWluaXN0cmF0b3IABFVzZXJSb2xlcwAhAAAAEDAAAQAAAAIxAA4AAABBZG1pbmlzdHJhdG9yAAAAAzIAtwAAAAUkSUQAEAAAAABlPjiPzn5UR4it6GRA5soKAiRUeXBlABYAAABTZWN1cml0eSREZW1vVXNlckltcGwAAkVudGl0eQAXAAAAQWRtaW5pc3RyYXRpb24uQWNjb3VudAACUGFzc3dvcmQACwAAAExkYzk5dWw3ZW0AAlVzZXJOYW1lAAoAAABkZW1vX3VzZXIABFVzZXJSb2xlcwAYAAAAEDAAAQAAAAIxAAUAAABVc2VyAAAAAzMAqAAAAAUkSUQAEAAAAAB1DpiL8iRIRr0PqiwBOHSAAiRUeXBlABYAAABTZWN1cml0eSREZW1vVXNlckltcGwAAkVudGl0eQAMAAAAU3lzdGVtLlVzZXIAAlBhc3N3b3JkAAsAAABXWWk3MGpoMnRjAAJVc2VyTmFtZQAGAAAARG9sbHkABFVzZXJSb2xlcwAYAAAAEDAAAQAAAAIxAAUAAABVc2VyAAAAAAhFbmFibGVEZW1vVXNlcnMAAQhFbmFibGVHdWVzdEFjY2VzcwAAA0ZpbGVEb2N1bWVudEFjY2VzcwBsAAAABSRJRAAQAAAAAAh2641J4YpGn/gkNTbQ268CJFR5cGUAKQAAAFNlY3VyaXR5JEZpbGVEb2N1bWVudEFjY2Vzc1J1bGVDb250YWluZXIABEFjY2Vzc1J1bGVzAAwAAAAQMAADAAAAAAACR3Vlc3RVc2VyUm9sZQABAAAAAANJbWFnZUFjY2VzcwBlAAAABSRJRAAQAAAAAFFnfJAHF1FEosrc/sJvlj4CJFR5cGUAIgAAAFNlY3VyaXR5JEltYWdlQWNjZXNzUnVsZUNvbnRhaW5lcgAEQWNjZXNzUnVsZXMADAAAABAwAAMAAAAAAANQYXNzd29yZFBvbGljeVNldHRpbmdzAI8AAAAFJElEABAAAAAA/X1IeABpUEW69ci0pIiCuAIkVHlwZQAgAAAAU2VjdXJpdHkkUGFzc3dvcmRQb2xpY3lTZXR0aW5ncwAQTWluaW11bUxlbmd0aAAGAAAACFJlcXVpcmVEaWdpdAABCFJlcXVpcmVNaXhlZENhc2UAAQhSZXF1aXJlU3ltYm9sAAAAAlNlY3VyaXR5TGV2ZWwAEAAAAENoZWNrRXZlcnl0aGluZwAEVXNlclJvbGVzAAwAAAAQMAACAAAAAAA=" + }, + { + "unit_id": "1fcc2823-c303-5a27-b6c7-78b04c96c88f", + "container_id": "82d1d7d0-ca49-4703-8557-1f478ee46d7c", + "module": null, + "containment": "ProjectConversion", + "name": "", + "type": "Projects$ProjectConversion", + "contents": "Yg4AAAUkSUQAEAAAAAAjKMwfA8MnWrbHeLBMlsiPAiRUeXBlABsAAABQcm9qZWN0cyRQcm9qZWN0Q29udmVyc2lvbgAET25lVGltZUNvbnZlcnNpb25zAAkOAAAQMAADAAAAAzEAbAAAAAUkSUQAEAAAAAA8Kn0UWf1WVZTmEg1dVx9JAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAdAAAAQ29udmVydEF0dHJpYnV0ZUNhcGFiaWxpdGllcwAAAzIAagAAAAUkSUQAEAAAAADbCWNleFa8UJj2+Vy+HSwEAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAbAAAAUGFnZVRlbXBsYXRlVHlwZUNvbnZlcnNpb24AAAMzAGcAAAAFJElEABAAAAAAAv0cJmZm11+RfgUW6ZjL1QIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGAAAAFBhZ2luZ0JhclR5cGVDb252ZXJzaW9uAAADNABvAAAABSRJRAAQAAAAAH19HfyYPihRi+JF6MTTMI8CJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACAAAABDb252ZXJ0Q29uc3VtZWRPRGF0YVNlcnZpY2VOYW1lAAADNQB6AAAABSRJRAAQAAAAAEwDKY3Hm81akbjsW/SChOACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACsAAABDb252ZXJ0UHVibGlzaGVkT0RhdGFTZXJ2aWNlVmVyc2lvbkRlZmF1bHQAAAM2AG4AAAAFJElEABAAAAAAvWvfA/KHhleVBw+300s3IwIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAE51bWJlck9mUGFnZXNUb0Nsb3NlQ29udmVyc2lvbgAAAzcAdwAAAAUkSUQAEAAAAAAmbvufHdJVVJFJX9ef57LcAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAoAAAAQ29udmVydENvbnN1bWVkT0RhdGFBdHRyaWJ1dGVSZW1vdGVUeXBlAAADOABjAAAABSRJRAAQAAAAANnUAx/sm75YlNV8BAIEwakCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABQAAABDb252ZXJ0T0RhdGFWZXJzaW9uAAADOQBrAAAABSRJRAAQAAAAACusz/OvC59XvKZ2jNNqovYCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABCb3R0b21CYXJJdGVtUGFnZUNvbnZlcnNpb24AAAMxMABrAAAABSRJRAAQAAAAAFJ1RDJhyVhdsmjdHjC3RDECJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABwAAABDb252ZXJ0Q2FwYWJpbGl0aWVzVG9Tb3VyY2UAAAMxMQBnAAAABSRJRAAQAAAAADnSOZNTDgxZhoveXAh+NWgCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lABgAAABBdGxhc0NhdGVnb3J5Q29udmVyc2lvbgAAAzEyAGgAAAAFJElEABAAAAAAteQbnrEJH12Gg0jEeA1WIQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAGQAAAENvbnZlcnRIdHRwQ29uZmlndXJhdGlvbgAAAzEzAHMAAAAFJElEABAAAAAAAw3SKO+fi1WUfSSyYD/eLQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJAAAAFByb2dyZXNzaXZlV2ViQXBwU2V0dGluZ3NDb252ZXJzaW9uAAADMTQAZAAAAAUkSUQAEAAAAABS7AyvmFOoUZdbLAU88/eAAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAVAAAATG9vcFNvdXJjZUNvbnZlcnNpb24AAAMxNQB1AAAABSRJRAAQAAAAALx5WxL8Z6xdqpT3gBccwjwCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACYAAABXb3JrZmxvd1N5c3RlbU1vZHVsZVJlbmFtZXNDb252ZXJzaW9uAAADMTYAeAAAAAUkSUQAEAAAAADWvAmoV5zlXaz5H+bSnOqeAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQApAAAARGVkdXBsaWNhdGVEZXBlbmRlbnRHdWlkT2JqZWN0Q29udmVyc2lvbgAAAzE3AHYAAAAFJElEABAAAAAA9dZKU2Epilmu3iP0pqBdZQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAENvbnZlcnRBcHBTZXJ2aWNlQWN0aXZpdHlEZWZhdWx0Q29sb3JzAAADMTgAeAAAAAUkSUQAEAAAAADtWiQx7iaZXI1qmSzgNxcBAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQApAAAARGVkdXBsaWNhdGVEZXNpZ25Qcm9wZXJ0eVZhbHVlQ29udmVyc2lvbgAAAzE5AG4AAAAFJElEABAAAAAAlQO7Md3PrFeoxF4Q/D6XXQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAHwAAAFdvcmtmbG93QWN0aXZpdHlOYW1lQ29udmVyc2lvbgAAAzIwAHgAAAAFJElEABAAAAAAMlg9L5LA8Vy384BZbqgPeQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKQAAAFdvcmtmbG93VXNlclRhc2tPbkNyZWF0ZWRFdmVudENvbnZlcnNpb24AAAMyMQBzAAAABSRJRAAQAAAAALXZsMBnHRNVp6M49+BdRmwCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACQAAABXb3JrZmxvd0RlZmF1bHRVc2VyRW50aXR5Q29udmVyc2lvbgAAAzIyAHYAAAAFJElEABAAAAAA1rAb7/sZzlGtGh4ZoMNNawIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAJwAAAFdvcmtmbG93RGVmYXVsdFF1ZXVlU2V0dGluZ3NDb252ZXJzaW9uAAADMjMAcwAAAAUkSUQAEAAAAABcFgvq1yKJWpeji6WSs23hAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAkAAAAV29ya2Zsb3dGaW5pc2hlZFN0YXRlTmFtZUNvbnZlcnNpb24AAAMyNABsAAAABSRJRAAQAAAAAGR3cwU0Ms1eqO1hHBnUWkACJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lAB0AAABDb252ZXJ0UHVibGlzaGVkT0RhdGFWZXJzaW9uAAADMjUAaQAAAAUkSUQAEAAAAAD6liuIXoIjVqg5AAgxY/aoAiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAaAAAAU3RhdGljUmVzb3VyY2VzQ29udmVyc2lvbgAAAzI2AHAAAAAFJElEABAAAAAAj9HdzsCa31qMWmhwUwBuqQIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAIQAAAE5hdGl2ZU5hbm9mbG93SG9tZVBhZ2VDb252ZXJzaW9uAAADMjcAbAAAAAUkSUQAEAAAAADNTBOEGm97Wp8YNzLfgMR2AiRUeXBlABsAAABQcm9qZWN0cyRPbmVUaW1lQ29udmVyc2lvbgACTmFtZQAdAAAAUXVldWVTeXN0ZW1Db250ZXh0Q29udmVyc2lvbgAAAzI4AHoAAAAFJElEABAAAAAAlfgcAoR6V1yLjn2js8lA/wIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKwAAAENvbnZlcnRPRGF0YUF0dHJpYnV0ZVVwZGF0YWJsZUNhcGFiaWxpdGllcwAAAzI5AHgAAAAFJElEABAAAAAA//LY1SvOl1CEnJncShDmUgIkVHlwZQAbAAAAUHJvamVjdHMkT25lVGltZUNvbnZlcnNpb24AAk5hbWUAKQAAAFdvcmtmbG93U3lzdGVtTW9kdWxlUmVuYW1lc0NvbnZlcnNpb25fdjIAAAMzMABxAAAABSRJRAAQAAAAAErz4SolQ7lUs5My5GIlntkCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACIAAABXb3JrZmxvd1ZhcmlhYmxlc1JlbmFtZUNvbnZlcnNpb24AAAMzMQBvAAAABSRJRAAQAAAAAAfCIWaANQZThm1vIPB64XgCJFR5cGUAGwAAAFByb2plY3RzJE9uZVRpbWVDb252ZXJzaW9uAAJOYW1lACAAAABSZW1vdmVXb3JrZmxvd0FkbWluaXN0cmF0b3JSb2xlAAAAAA==" + } + ] +} diff --git a/lib/mxrb/widget_package.rb b/lib/mxrb/widget_package.rb index aeabb42..587f639 100644 --- a/lib/mxrb/widget_package.rb +++ b/lib/mxrb/widget_package.rb @@ -61,23 +61,24 @@ def template(definition) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength end type = { '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$CustomWidgetType', - 'HelpUrl' => definition.help_url, 'OfflineCapable' => definition.offline, + 'HelpUrl' => definition.help_url, + 'ObjectType' => { + '$ID' => object_type_id, '$Type' => 'CustomWidgets$WidgetObjectType', + 'PropertyTypes' => IO::BsonCodec.build_array(property_types, marker: 2) + }, + 'OfflineCapable' => definition.offline, 'StudioCategory' => definition.studio_category, 'StudioProCategory' => definition.studio_pro_category, 'SupportedPlatform' => definition.platform, 'WidgetDescription' => definition.description, 'WidgetId' => definition.id, 'WidgetName' => definition.name, 'WidgetNeedsEntityContext' => definition.needs_context, - 'WidgetPluginWidget' => definition.plugin, - 'ObjectType' => { - '$ID' => object_type_id, '$Type' => 'CustomWidgets$WidgetObjectType', - 'PropertyTypes' => IO::BsonCodec.build_array(property_types, marker: 2) - } + 'WidgetPluginWidget' => definition.plugin } object = { '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetObject', - 'TypePointer' => object_type_id, - 'Properties' => IO::BsonCodec.build_array(properties, marker: 2) + 'Properties' => IO::BsonCodec.build_array(properties, marker: 2), + 'TypePointer' => object_type_id } [type, object] end @@ -108,33 +109,20 @@ def build_definition(root) # rubocop:disable Metrics/AbcSize, Metrics/MethodLeng ) end - # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength def property_groups(parent, category) parent.elements.to_a.flat_map do |element| - next [] unless element.name == 'propertyGroup' - - group = [category, element.attributes['caption']].compact.reject(&:empty?).join('::') - element.elements.to_a.flat_map do |member| - case member.name - when 'property' then [property(member, group)] - when 'systemProperty' then [system_property(member, group)] - when 'propertyGroup' then property_groups_wrapper(member, group) - else [] - end - end + property_member(element, category) end end - # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength - - def property_groups_wrapper(group, parent_category) - category = [parent_category, group.attributes['caption']].compact.reject(&:empty?).join('::') - group.elements.to_a.flat_map do |member| - case member.name - when 'property' then [property(member, category)] - when 'systemProperty' then [system_property(member, category)] - when 'propertyGroup' then property_groups_wrapper(member, category) - else [] - end + + def property_member(element, category) + case element.name + when 'property' then [property(element, category.to_s.empty? ? 'General' : category.to_s)] + when 'systemProperty' then [system_property(element, category.to_s.empty? ? 'General' : category.to_s)] + when 'propertyGroup' + nested = [category, element.attributes['caption']].compact.reject(&:empty?).join('::') + property_groups(element, nested) + else [] end end @@ -147,6 +135,17 @@ def property(element, category) # rubocop:disable Metrics/AbcSize, Metrics/Metho list: element.attributes['isList'] == 'true', multiline: element.attributes['multiline'] == 'true', data_source: element.attributes['dataSource'].to_s, on_change: element.attributes['onChange'].to_s, + default_type: element.attributes['defaultType'].to_s, + linked: element.attributes['isLinked'] == 'true', + metadata: element.attributes['isMetaData'] == 'true', + selectable_objects: element.attributes['selectableObjects'].to_s, + set_label: element.attributes['setLabel'] == 'true', + action_variables: nested_elements(element, 'actionVariables', 'actionVariable').map do |variable| + { + key: variable.attributes['key'].to_s, caption: variable.attributes['caption'].to_s, + type: variable.attributes['type'].to_s + } + end, allowed_types: nested_element_names(element, 'attributeTypes', 'attributeType'), association_types: nested_element_names(element, 'associationTypes', 'associationType'), selection_types: nested_element_names(element, 'selectionTypes', 'selectionType'), @@ -164,7 +163,9 @@ def system_property(element, category) { key: element.attributes['key'], type: 'System', caption: "", description: '', category:, required: false, default: '', list: false, multiline: false, data_source: '', on_change: '', allowed_types: [], association_types: [], selection_types: [], - enum_values: [], translations: [], return_type: nil, children: [], system: true } + enum_values: [], translations: [], return_type: nil, children: [], system: true, + default_type: '', linked: false, metadata: false, selectable_objects: '', set_label: false, + action_variables: [] } end def nested_properties(element) @@ -203,21 +204,22 @@ def value_type(property) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength type = property[:type] { '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetValueType', - 'ActionVariables' => IO::BsonCodec.build_array([], marker: 2), + 'ActionVariables' => action_variables(property[:action_variables]), 'AllowedTypes' => IO::BsonCodec.build_array(property[:allowed_types], marker: 1), 'AllowNonPersistableEntities' => false, 'AllowUpload' => false, 'AssociationTypes' => IO::BsonCodec.build_array(property[:association_types], marker: 1), - 'DataSourceProperty' => property[:data_source], 'DefaultType' => 'None', + 'DataSourceProperty' => property[:data_source], + 'DefaultType' => property[:default_type].empty? ? 'None' : property[:default_type], 'DefaultValue' => property[:default], 'EntityProperty' => '', 'EnumerationValues' => enumeration_values(property[:enum_values]), - 'IsLinked' => false, 'IsList' => property[:list], 'IsMetaData' => false, + 'IsLinked' => property[:linked], 'IsList' => property[:list], 'IsMetaData' => property[:metadata], 'IsPath' => 'No', 'Multiline' => property[:multiline], 'ObjectType' => object_type(property[:children]), 'OnChangeProperty' => property[:on_change], 'ParameterIsList' => false, 'PathType' => 'None', 'Required' => property[:required], 'ReturnType' => widget_return_type(property[:return_type]), - 'SelectableObjectsProperty' => '', + 'SelectableObjectsProperty' => property[:selectable_objects], 'SelectionTypes' => IO::BsonCodec.build_array(property[:selection_types], marker: 1), - 'SetLabel' => false, 'Translations' => widget_translations(property[:translations]), + 'SetLabel' => property[:set_label], 'Translations' => widget_translations(property[:translations]), 'Type' => type } end @@ -246,6 +248,16 @@ def widget_translations(values) end, marker: 2) end + def action_variables(values) + IO::BsonCodec.build_array(values.map do |variable| + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetActionVariable', + 'Caption' => variable.fetch(:caption), 'Key' => variable.fetch(:key), + 'Type' => variable.fetch(:type) + } + end, marker: 2) + end + def widget_return_type(value) return unless value @@ -270,12 +282,16 @@ def widget_value(value_type, property) # rubocop:disable Metrics/MethodLength end # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity + # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity def default_widget_value(value, property) case property[:type] when 'Boolean' then value['PrimitiveValue'] = property[:default].empty? ? 'false' : property[:default] when 'Integer' then value['PrimitiveValue'] = property[:default].empty? ? '0' : property[:default] when 'Enumeration' then value['PrimitiveValue'] = property[:default] + when 'String', 'Decimal' then value['PrimitiveValue'] = property[:default] when 'Expression' then value['Expression'] = property[:default] + when 'Selection' + value['Selection'] = property[:default].empty? ? property[:selection_types].first || 'None' : property[:default] when 'TextTemplate' value['TextTemplate'] = client_template(property[:translations]) \ if property[:required] || !property[:default].empty? || !property[:translations].empty? @@ -283,6 +299,7 @@ def default_widget_value(value, property) value end # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity + # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity def client_template(translations) items = translations.map do |language, value| @@ -316,6 +333,6 @@ def nested_element_names(root, container, name) def child(root, name) = root.elements.to_a.find { _1.name == name } - def text(root, name) = child(root, name)&.text.to_s.strip + def text(root, name) = child(root, name)&.text.to_s end end diff --git a/lib/mxrb/writer.rb b/lib/mxrb/writer.rb index 0ec54d8..c7b2146 100644 --- a/lib/mxrb/writer.rb +++ b/lib/mxrb/writer.rb @@ -259,16 +259,47 @@ def apply_default_project_units(mpr, root_id) existing = mpr.children_of(root_id).map { mpr.parse_contents(_1)['$Type'] } units = load_native_units(path).reject { existing.include?(_1.fetch('type')) } units.each do |unit| - next unless unit.fetch('type') == 'Settings$ProjectSettings' - - array_items(unit.dig('doc', 'Settings')).each do |setting| - setting['EnableNewStringBehavior'] = true \ - if setting['$Type'] == 'Forms$WebUIProjectSettingsPart' + case unit.fetch('type') + when 'Settings$ProjectSettings' + sanitize_project_settings!(unit.fetch('doc')) + when 'Navigation$NavigationDocument' + sanitize_project_navigation!(unit.fetch('doc')) end end apply_native_unit_tree(mpr, root_id, units) end + def sanitize_project_navigation!(document) + profiles = array_items(document['Profiles']) + profiles += LEGACY_NAVIGATION_PROFILES.values.filter_map { document[_1] } + profiles.each do |profile| + # Icons stored in a donor project refer to image collections that are + # not part of a structural project template. The Ruby navigation DSL + # may set a new icon explicitly after the clean seed is installed. + profile['AppIcon'] = '' if profile.key?('AppIcon') + end + document + end + + def sanitize_project_settings!(document) + array_items(document['Settings']).each do |setting| + case setting['$Type'] + when 'Forms$WebUIProjectSettingsPart' + setting['EnableNewStringBehavior'] = true + setting['UseOptimizedClient'] = 'Yes' if @definition.fetch(:version).to_i == 10 + setting['ThemeModuleName'] = '' if setting.key?('ThemeModuleName') + when 'Settings$ModelSettings' + # A project template is structural seed data, not an application + # baseline. Lifecycle callbacks from the donor project must never + # leak into a standalone Ruby-first project. + %w[AfterStartupMicroflow BeforeShutdownMicroflow HealthCheckMicroflow].each do |key| + setting[key] = '' if setting.key?(key) + end + end + end + document + end + def native_module_doc(native_units, module_name) native_units.find do |unit| unit["containment"] == "Modules" && @@ -283,13 +314,48 @@ def apply_native_module_units(mpr, module_id, module_name, native_units) def write_native_documents(mpr, module_id, mod) mod.fetch(:native_documents, []).each do |document| + doc = document.fetch(:doc) + doc = legacy_layout_doc(doc) if doc['$Type'] == 'Forms$Layout' && legacy_layout? upsert_native_unit( mpr, module_id, - 'containment' => document.fetch(:containment), 'doc' => document.fetch(:doc) + 'containment' => document.fetch(:containment), 'doc' => doc ) end end + def legacy_layout? + @definition.fetch(:version).to_i < 9 + end + + def legacy_layout_doc(source) + placeholder = { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Placeholder', + 'Class' => '', 'Name' => 'Main', 'Style' => '', 'TabIndex' => 0 + } + common = { + '$ID' => source['$ID'] || SecureRandom.uuid, '$Type' => 'Forms$Layout', + 'CanvasHeight' => source.fetch('CanvasHeight', 600), + 'CanvasWidth' => source.fetch('CanvasWidth', 800), + 'Class' => source.dig('Appearance', 'Class').to_s, + 'Documentation' => source.fetch('Documentation', ''), 'Excluded' => false, + 'FormCall' => nil, 'LayoutType' => 'Responsive', 'Name' => source.fetch('Name'), + 'Style' => source.dig('Appearance', 'Style').to_s + } + return common.merge('Widgets' => IO::BsonCodec.build_array([placeholder], marker: 2)) \ + unless oldest_layout_contract? + + common.merge( + 'AcceptPlaceholderName' => '', 'CancelPlaceholderName' => '', + 'MainPlaceholderName' => 'Main', 'UseMainPlaceholderForPopups' => false, + 'Widget' => placeholder + ) + end + + def oldest_layout_contract? + major, minor = @definition.fetch(:version).split('.').map(&:to_i) + major < 7 || (major == 7 && minor < 17) + end + def apply_native_unit_tree(mpr, target_root_id, units) if units.any? { _1["unit_id"].to_s.empty? || _1["container_id"].to_s.empty? } units.each { upsert_native_unit(mpr, target_root_id, _1) } @@ -354,6 +420,15 @@ def write_project_security(mpr, root_id, security) "UserRoles" => doc.fetch("UserRoles"), "AdminUserRole" => doc.fetch("AdminUserRole") } + if existing.key?("UserRoles") && array_items(existing["UserRoles"]).empty? + # Project templates may contain demo users for modules and roles that + # are intentionally not copied into a new Ruby-first project. Once + # the empty role set is bootstrapped, those references must be + # removed as one atomic security repair. + editable["DemoUsers"] = doc.fetch("DemoUsers") + editable["EnableDemoUsers"] = doc.fetch("EnableDemoUsers") + editable["SecurityLevel"] = doc.fetch("SecurityLevel") + end if security[:security_level] editable["SecurityLevel"] = doc.fetch("SecurityLevel") end @@ -400,11 +475,31 @@ def legacy_navigation?(document) def legacy_navigation_doc(existing, navigation) navigation.fetch(:profiles, []).each_with_object(existing.dup) do |profile, document| - key = LEGACY_NAVIGATION_PROFILES.fetch(profile.fetch(:name).to_s) - document[key] = navigation_profile_doc(profile, previous: existing[key]) + name = profile.fetch(:name).to_s + name = 'Desktop' if name == 'Responsive' + key = LEGACY_NAVIGATION_PROFILES.fetch(name) + document[key] = legacy_navigation_profile_doc(profile, previous: existing[key]) end end + def legacy_navigation_profile_doc(profile, previous:) + previous ||= {} + role_homes = profile.fetch(:role_homes, {}).map do |role, page| + { role: role.to_s, page: page.to_s } + end + profile.fetch(:role_home_details, []) + title = profile.fetch(:app_title, {}) + title = title.values.first.to_s if title.is_a?(Hash) + editable = { + 'HomePage' => navigation_home_doc(profile[:home_page], profile[:home_microflow]), + 'HomeItems' => IO::BsonCodec.build_array(role_homes.map { navigation_role_home_doc(_1) }, marker: 2), + 'Menu' => navigation_menu_doc(profile.fetch(:items, [])), + 'Enabled' => true, + 'ApplicationTitle' => title.to_s.empty? ? previous.fetch('ApplicationTitle', 'Mendix') : title.to_s + } + editable['Kind'] = profile[:kind].to_s unless profile[:kind].to_s.empty? + previous.merge(editable) + end + def modern_navigation_doc(existing, navigation) existing_profiles = array_items(existing["Profiles"]).to_h { [_1["Name"].to_s, _1] } profiles = navigation.fetch(:profiles, []).map do |profile| @@ -551,9 +646,11 @@ def translated_text_doc(translations) def write_module_security(mpr, module_id, mod) raw = mpr.children_of(module_id).find { _1["ContainmentName"] == "ModuleSecurity" } - return if raw.nil? && mod.fetch(:module_roles, []).empty? + return if raw.nil? && mod.fetch(:module_roles, []).empty? && @definition.fetch(:version).to_i >= 10 - doc = module_security_doc(mod.fetch(:module_roles, [])) + doc = module_security_doc( + mod.fetch(:module_roles, []), legacy: @definition.fetch(:version).to_i < 10 + ) if raw existing = mpr.parse_contents(raw) doc = existing.merge(doc) @@ -624,7 +721,8 @@ def write_domain_model(mpr, module_id, mod) doc = existing.merge( "$ID" => raw&.fetch("UnitID", nil) || SecureRandom.uuid, - "$Type" => "DomainModels$DomainModel" + "$Type" => "DomainModels$DomainModel", + "Documentation" => existing.fetch("Documentation", "") ) doc[entities_key] = IO::BsonCodec.build_array(entities) doc[associations_key] = IO::BsonCodec.build_array(associations) @@ -1372,20 +1470,31 @@ def validation_rules_doc(entity, module_name, previous) end return previous || IO::BsonCodec.build_array([]) if declarations.empty? - declared_names = declarations.map { _1.fetch(:name) } kept = existing.reject do |rule| - declared_names.include?(rule['Attribute'].to_s.split('.').last) && - rule.dig('RuleInfo', '$Type').to_s.match?(/(?:Required|Unique)RuleInfo\z/) + declarations.any? { validation_declaration_matches?(_1, rule) } end generated = declarations.flat_map do |attribute| %i[required unique].filter_map do |kind| - validation_rule_doc(module_name, entity.fetch(:name), attribute.fetch(:name), kind) \ - if attribute[kind] == true + next unless attribute[kind] == true + + existing.find { validation_rule_matches?(_1, attribute.fetch(:name), kind) } || + validation_rule_doc(module_name, entity.fetch(:name), attribute.fetch(:name), kind) end end IO::BsonCodec.build_array(kept + generated) end + def validation_declaration_matches?(attribute, rule) + %i[required unique].any? do |kind| + attribute.key?(kind) && validation_rule_matches?(rule, attribute.fetch(:name), kind) + end + end + + def validation_rule_matches?(rule, attribute_name, kind) + rule['Attribute'].to_s.split('.').last == attribute_name && + rule.dig('RuleInfo', '$Type').to_s.end_with?("#{kind.to_s.capitalize}RuleInfo") + end + def validation_rule_doc(module_name, entity_name, attribute_name, kind) { '$ID' => SecureRandom.uuid, '$Type' => 'DomainModels$ValidationRule', @@ -1458,16 +1567,21 @@ def exact_access_member_docs(members, module_name, entity_name) members.map do |member| association = member.fetch(:kind, :attribute).to_sym == :association name = member.fetch(:name).to_s + reference = member[:reference].to_s { "$ID" => SecureRandom.uuid, "$Type" => "DomainModels$MemberAccess", - "Association" => association ? "#{module_name}.#{name}" : "", - "Attribute" => association ? "" : "#{module_name}.#{entity_name}.#{name}", + "Association" => association ? qualified_member(reference, "#{module_name}.#{name}") : "", + "Attribute" => association ? "" : qualified_member(reference, "#{module_name}.#{entity_name}.#{name}"), "AccessRights" => member.fetch(:rights).to_s } end end + def qualified_member(reference, fallback) + reference.empty? ? fallback : reference + end + def access_default_rights(read, write) return "ReadWrite" if write == :all return "ReadOnly" if read == :all @@ -1578,14 +1692,20 @@ def page_doc(page) else widgets end + content = content.map { legacy_widget_tree(_1) } if oldest_layout_contract? roles_declared = !page[:allowed_roles].nil? + argument = { + "$ID" => SecureRandom.uuid, "$Type" => "Forms$FormCallArgument", + "Parameter" => "#{page.fetch(:layout)}.Main" + } + if oldest_layout_contract? + argument['Widget'] = legacy_single_widget(content) + else + argument['Widgets'] = IO::BsonCodec.build_array(content, marker: 2) + end form_call = { "$ID" => SecureRandom.uuid, "$Type" => "Forms$LayoutCall", - "Arguments" => IO::BsonCodec.build_array([{ - "$ID" => SecureRandom.uuid, "$Type" => "Forms$FormCallArgument", - "Parameter" => "#{page.fetch(:layout)}.Main", - "Widgets" => IO::BsonCodec.build_array(content, marker: 2) - }], marker: 2), + "Arguments" => IO::BsonCodec.build_array([argument], marker: 2), "Form" => page.fetch(:layout) } doc = { "$ID" => SecureRandom.uuid, "$Type" => "Forms$Page", "Name" => page.fetch(:name), @@ -1601,6 +1721,38 @@ def page_doc(page) doc end + def legacy_widget_tree(value) + transformed = value.transform_values do |child| + case child + when Hash then legacy_widget_tree(child) + when Array then child.map { _1.is_a?(Hash) ? legacy_widget_tree(_1) : _1 } + else child + end + end + singular = { + 'Forms$DivContainer' => [%w[Widgets Widget]], + 'Forms$DataView' => [%w[Widgets Widget], %w[FooterWidgets FooterWidget]], + 'Forms$TabPage' => [%w[Widgets Widget]] + }.fetch(transformed['$Type'], []) + singular.each do |plural, single| + next unless transformed.key?(plural) + + transformed[single] = legacy_single_widget(array_items(transformed.delete(plural))) + end + transformed + end + + def legacy_single_widget(widgets) + return nil if widgets.empty? + return widgets.first if widgets.one? + + { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$VerticalFlow', + 'Widgets' => IO::BsonCodec.build_array(widgets, marker: 2), + 'Name' => 'verticalFlow', 'Class' => '', 'Style' => '', 'TabIndex' => 0 + } + end + def menu_doc(menu) if menu[:deep_structure].is_a?(Hash) return menu[:deep_structure].merge( @@ -1783,11 +1935,13 @@ def user_role_doc(role) } end - def module_security_doc(roles) + def module_security_doc(roles, legacy: false) { "$ID" => SecureRandom.uuid, "$Type" => "Security$ModuleSecurity", - "ModuleRoles" => IO::BsonCodec.build_array(roles.map { module_role_doc(_1) }, marker: 2) + "ModuleRoles" => IO::BsonCodec.build_array( + roles.map { module_role_doc(_1) }, marker: legacy ? 3 : 2 + ) } end diff --git a/script/extract_project_template b/script/extract_project_template new file mode 100755 index 0000000..c572960 --- /dev/null +++ b/script/extract_project_template @@ -0,0 +1,56 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +require 'base64' +require 'json' +require_relative '../lib/mxrb' + +model, version, output = ARGV +unless model && version + warn 'Usage: script/extract_project_template MODEL.MPR VERSION [OUTPUT]' + exit 64 +end +output ||= File.join(__dir__, '..', 'lib', 'mxrb', 'templates', 'project', "#{version}.json") + +types = %w[ + Settings$ProjectSettings Texts$SystemTextCollection Projects$ProjectConversion + Navigation$NavigationDocument Security$ProjectSecurity +].freeze + +sanitize = lambda do |document| + case document['$Type'] + when 'Navigation$NavigationDocument' + document.each do |key, profile| + next unless key.end_with?('Profile') && profile.is_a?(Hash) + + profile['HomePage']&.merge!('Page' => '', 'Microflow' => '') + profile['HomeItems'] = [profile.fetch('HomeItems', [2]).first] + profile.dig('Menu', 'Items')&.replace([profile.dig('Menu', 'Items').first]) + end + when 'Security$ProjectSecurity' + document['UserRoles'] = [document.fetch('UserRoles', [1]).first] + document['AdminUserRole'] = '' + document['GuestUserRole'] = '' if document.key?('GuestUserRole') + end + document +end +mpr = Mxrb::IO::MprFile.open(model, readonly: true) +root_id = mpr.root_unit.fetch('UnitID') +units = mpr.children_of(root_id).filter_map do |unit| + document = sanitize.call(mpr.parse_contents(unit)) + next unless types.include?(document['$Type']) + + { + 'unit_id' => unit.fetch('UnitID'), 'container_id' => unit.fetch('ContainerID'), + 'module' => nil, 'containment' => unit.fetch('ContainmentName'), + 'name' => document['Name'].to_s, 'type' => document.fetch('$Type'), + 'contents' => Base64.strict_encode64(Mxrb::IO::BsonCodec.serialize(document)) + } +end +mpr.close + +missing = types - units.map { _1.fetch('type') } +raise Mxrb::SerializationError, "project template is missing #{missing.join(', ')}" unless missing.empty? + +File.write(output, "#{JSON.pretty_generate('format_version' => 'v1', 'units' => units)}\n") +puts "Project template #{version}: #{units.length} units, output=#{File.expand_path(output)}" diff --git a/script/frontend_acceptance b/script/frontend_acceptance new file mode 100755 index 0000000..7bdc16e --- /dev/null +++ b/script/frontend_acceptance @@ -0,0 +1,572 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +# rubocop:disable Metrics + +require 'digest' +require 'fileutils' +require 'json' +require 'open3' +require 'optparse' +require 'tmpdir' +$LOAD_PATH.unshift(File.expand_path('../lib', __dir__)) +require_relative '../lib/mxrb' + +# Fail-closed frontend acceptance for the explicitly supported Mendix 10/11 +# matrix. MxBuild is never used to generate or mutate the project: when +# configured, it is a read-only external acceptance oracle. +module MxrbFrontendAcceptance + class Failure < StandardError; end + + CATEGORY_BY_CODE = { + 'CE0161' => 'host_schema', + 'CE0462' => 'missing_widget_package', + 'CE0463' => 'stale_widget_definition', + 'CE0527' => 'missing_navigation_home', + 'CE0535' => 'invalid_layout_grid', + 'CE0635' => 'building_block_reference', + 'CE1613' => 'missing_navigation_profile', + 'CE6087' => 'stale_design_properties', + 'CW0055' => 'missing_click_action', + 'CW0094' => 'unused_variable', + 'CW0263' => 'missing_translation' + }.freeze + ASSET_DIRECTORIES = Mxrb::Model::DesignSystem::ASSET_DIRECTORIES.freeze + ASSET_PATHS = (ASSET_DIRECTORIES + [ + '.mxrb/marketplace.lock.json', '.mxrb/marketplace', '.mxrb/marketplace-originals' + ]).freeze + MARKETPLACE_GLOBS = [ + '.mxrb/marketplace.lock.json', '.mxrb/marketplace/**/*', '.mxrb/marketplace-originals/**/*' + ].freeze + + # Strict parser, classifier, and semantic comparator for MxBuild JSON. + class DiagnosticSet + SEVERITIES = %w[error warning].freeze + CHECK_ARRAYS = { + 'errors' => %w[Error error], + 'warnings' => %w[Warning warning], + 'deprecations' => %w[Warning deprecation] + }.freeze + + attr_reader :problems + + def self.read(path) + new(JSON.parse(File.read(path))) + rescue JSON::ParserError => e + raise Failure, "#{path}: invalid MxBuild JSON: #{e.message}" + end + + def self.success_without_diagnostics + new({ 'problems' => [] }, warnings_observable: false) + end + + def initialize(payload, warnings_observable: true) + raise Failure, 'MxBuild result must be a JSON object' unless payload.is_a?(Hash) + + payload = normalize_check_payload(payload) unless payload.key?('problems') + raise Failure, 'MxBuild result has no problems array' unless payload.key?('problems') + + @problems = payload.fetch('problems') + @warnings_observable = warnings_observable + raise Failure, 'MxBuild problems must be an array' unless @problems.is_a?(Array) + + @problems.each { validate_problem(_1) } + end + + def equivalent?(other) + signatures == other.signatures + end + + def ready?(strict_warnings: false) + errors.zero? && (!strict_warnings || (warnings_observable? && warnings.zero?)) + end + + def warnings_observable? = @warnings_observable + + def exit_mask + kinds = @problems.map { problem_kind(_1) } + (kinds.include?('error') ? 1 : 0) | + (kinds.include?('warning') ? 2 : 0) | + (kinds.include?('deprecation') ? 4 : 0) | + (kinds.include?('best_practice') ? 8 : 0) + end + + def errors + @problems.count { _1.fetch('severity').casecmp?('error') } + end + + def warnings + return unless warnings_observable? + + @problems.count { _1.fetch('severity').casecmp?('warning') } + end + + def signatures + @signatures ||= @problems.map { signature(_1) }.sort.freeze + end + + def to_h + { + problems: warnings_observable? ? @problems.size : nil, + errors:, + warnings:, + warnings_observable: warnings_observable?, + kinds: grouped_counts { problem_kind(_1) }, + categories: grouped_counts { category(_1) }, + codes: grouped_counts { _1.fetch('errorCode') }, + modules: grouped_locations('module'), + elements: grouped_locations('element') + } + end + + private + + def normalize_check_payload(payload) + keys = CHECK_ARRAYS.keys + %w[performance total-problems] + return payload unless keys.all? { payload.key?(_1) } + + problems = CHECK_ARRAYS.flat_map do |key, (severity, kind)| + normalize_check_array(payload.fetch(key), severity:, kind:, key:) + end + performance = payload.fetch('performance') + raise Failure, 'mx check performance must be an object' unless performance.is_a?(Hash) + + recommendation_options = { + severity: 'Warning', kind: 'best_practice', key: 'performance recommendations' + } + problems.concat( + normalize_check_array(performance.fetch('recommendations', []), **recommendation_options) + ) + total = payload.fetch('total-problems') + unless total.is_a?(Integer) && total == problems.size + raise Failure, "mx check total-problems mismatch: #{total.inspect} != #{problems.size}" + end + + { 'problems' => problems } + end + + def normalize_check_array(values, severity:, kind:, key:) + raise Failure, "mx check #{key} must be an array" unless values.is_a?(Array) + + values.map do |problem| + raise Failure, "each mx check #{key} entry must be an object" unless problem.is_a?(Hash) + + { + 'severity' => severity, 'kind' => kind, + 'errorCode' => problem.fetch('code', ''), 'message' => problem.fetch('message', ''), + 'locations' => normalize_check_locations(problem.fetch('locations', [])) + } + end + end + + def normalize_check_locations(locations) + raise Failure, 'mx check problem locations must be an array' unless locations.is_a?(Array) + + locations.map do |location| + raise Failure, 'each mx check location must be an object' unless location.is_a?(Hash) + + { + 'module' => location['module-name'], 'document' => location['document-name'], + 'element' => location['element-name'], 'unitId' => location['unit-id'], + 'elementId' => location['element-id'] + } + end + end + + def validate_problem(problem) + raise Failure, 'each MxBuild problem must be an object' unless problem.is_a?(Hash) + + %w[severity message errorCode].each do |field| + raise Failure, "MxBuild problem has no #{field}" unless problem[field].is_a?(String) + end + unless SEVERITIES.include?(problem.fetch('severity').downcase) + raise Failure, "unknown MxBuild severity: #{problem.fetch('severity').inspect}" + end + raise Failure, 'MxBuild problem has an empty errorCode' if problem.fetch('errorCode').empty? + + locations = problem.fetch('locations', []) + raise Failure, 'MxBuild problem locations must be an array' unless locations.is_a?(Array) + raise Failure, 'each MxBuild location must be an object' unless locations.all?(Hash) + end + + def signature(problem) + locations = problem.fetch('locations', []).map do |location| + %w[module document element].map { location[_1].to_s } + end.sort + JSON.generate([ + problem.fetch('severity').downcase, + problem_kind(problem), problem.fetch('errorCode'), problem.fetch('message'), locations + ]) + end + + def problem_kind(problem) + problem.fetch('kind', problem.fetch('severity').downcase) + end + + def category(problem) + CATEGORY_BY_CODE.fetch(problem.fetch('errorCode'), 'unclassified') + end + + def grouped_counts + @problems.group_by { yield(_1) }.transform_values(&:size).sort.to_h + end + + def grouped_locations(field) + @problems.flat_map { _1.fetch('locations', []).map { |location| location[field].to_s } } + .reject(&:empty?).tally.sort.to_h + end + end + + # Verifies complete asset inventories, bytes, checksums, and lifecycle data. + class AssetGate + def initialize(source_root, exported_root, rebuilt_root) + @source_root = source_root + @exported_root = exported_root + @rebuilt_root = rebuilt_root + end + + def inspect + entries = read_manifest + paths = entries.map { validate_entry(_1) } + raise Failure, 'asset manifest contains duplicate paths' unless paths.uniq.size == paths.size + + expected = paths.sort + verify_inventory(@source_root, expected, 'source') + verify_inventory(@exported_root, expected, 'Ruby export') + verify_inventory(@rebuilt_root, expected, 'rebuilt project') + entries.each { verify_entry(_1, @source_root, @exported_root, @rebuilt_root) } + provenance = verify_marketplace_provenance + { + passed: %i[missing changed unexpected].all? { provenance.fetch(_1).empty? }, files: entries.size, + bytes: entries.sum { _1.fetch('size') }, marketplace_provenance: provenance + } + end + + private + + def read_manifest + path = File.join(@exported_root, '.mxrb', 'assets.json') + payload = JSON.parse(File.read(path)) + raise Failure, 'asset manifest version must be 1' unless payload['version'] == 1 + raise Failure, 'asset manifest files must be an array' unless payload['files'].is_a?(Array) + + payload.fetch('files') + rescue Errno::ENOENT + raise Failure, 'Ruby export has no .mxrb/assets.json' + rescue JSON::ParserError => e + raise Failure, "invalid asset manifest: #{e.message}" + end + + def validate_entry(entry) + raise Failure, 'asset manifest entry must be an object' unless entry.is_a?(Hash) + + path = entry['path'] + size = entry['size'] + digest = entry['sha256'] + pathname = Pathname.new(path.to_s) + safe = path.is_a?(String) && !path.empty? && !pathname.absolute? && + !pathname.each_filename.include?('..') + raise Failure, "unsafe asset path: #{path.inspect}" unless safe + raise Failure, "invalid asset size: #{path}" unless size.is_a?(Integer) && size >= 0 + raise Failure, "invalid asset checksum: #{path}" unless digest.to_s.match?(/\A[0-9a-f]{64}\z/) + + path + end + + def inventory(root) + candidates = ASSET_PATHS.flat_map do |relative| + path = File.join(root, relative) + next [path] if File.file?(path) || File.symlink?(path) + + Dir.glob(File.join(path, '**', '*'), File::FNM_DOTMATCH) + end + symlinks = candidates.select { File.symlink?(_1) } + unless symlinks.empty? + paths = symlinks.map { Pathname.new(_1).relative_path_from(Pathname.new(root)).to_s }.sort + raise Failure, "asset inventory contains symlinks: #{paths.join(', ')}" + end + + candidates.select { File.file?(_1) && File.basename(_1) != '.keep' } + .map { Pathname.new(_1).relative_path_from(Pathname.new(root)).to_s }.uniq.sort + end + + def verify_inventory(root, expected, label) + actual = inventory(root) + return if actual == expected + + raise Failure, "#{label} asset inventory differs: " \ + "missing=#{expected - actual}, untracked=#{actual - expected}" + end + + def verify_entry(entry, *roots) + roots.each do |root| + path = File.join(root, entry.fetch('path')) + raise Failure, "missing asset: #{path}" unless File.file?(path) && !File.symlink?(path) + raise Failure, "asset size mismatch: #{path}" unless File.size(path) == entry.fetch('size') + next if Digest::SHA256.file(path).hexdigest == entry.fetch('sha256') + + raise Failure, "asset checksum mismatch: #{path}" + end + end + + def marketplace_paths(root) + MARKETPLACE_GLOBS.flat_map { Dir.glob(File.join(root, _1)) } + .select { File.file?(_1) && !File.symlink?(_1) } + .map { Pathname.new(_1).relative_path_from(Pathname.new(root)).to_s }.uniq.sort + end + + def verify_marketplace_provenance + source = marketplace_paths(@source_root) + rebuilt = marketplace_paths(@rebuilt_root) + changed = (source & rebuilt).reject do |path| + Digest::SHA256.file(File.join(@source_root, path)).hexdigest == + Digest::SHA256.file(File.join(@rebuilt_root, path)).hexdigest + end + { files: source.size, missing: source - rebuilt, changed:, unexpected: rebuilt - source } + end + end + + # Executes MxBuild outside the generation path and captures diagnostics only. + class MxBuildOracle + def initialize(executable) + @executable = File.expand_path(executable) + raise Failure, "MxBuild is not executable: #{@executable}" unless File.executable?(@executable) + end + + def diagnose(mpr, root) + FileUtils.mkdir_p(root) + output = File.join(root, 'app.mda') + errors = File.join(root, 'mxbuild-errors.json') + java_version = Mxrb::Runtime::Toolchain.new(mpr).plan.java_version + java_home = Mxrb::Runtime::JavaLocator.resolve(java_version) + raise Failure, "Java #{java_version} is required by MxBuild" unless java_home + + command = [ + @executable, "--java-home=#{java_home}", + "--java-exe-path=#{File.join(java_home, 'bin', 'java')}", '--target=package', + "--output=#{output}", "--write-errors=#{errors}", mpr + ] + transcript, status = Open3.capture2e(*command, chdir: File.dirname(mpr)) + diagnostics = DiagnosticSet.read(errors) if File.file?(errors) + if status.success? + unless File.file?(output) && File.size?(output) + raise Failure, 'MxBuild succeeded without producing a non-empty MDA' + end + + return diagnostics || DiagnosticSet.success_without_diagnostics + end + return diagnostics if diagnostics&.errors&.positive? + + detail = transcript.lines.last(20).join.strip + raise Failure, "MxBuild exited #{status.exitstatus.inspect} without model errors: #{detail}" + end + end + + # Runs the official read-only checker so warnings, deprecations, and best + # practices remain observable even when a successful MxBuild emits no JSON. + class MxCheckOracle + def initialize(executable) + @executable = File.expand_path(executable) + raise Failure, "mx is not executable: #{@executable}" unless File.executable?(@executable) + end + + def diagnose(mpr, root) + FileUtils.mkdir_p(root) + output = File.join(root, 'mx-check.json') + command = [ + @executable, 'check', mpr, '--warnings', '--deprecations', '--best-practice', '--json', output + ] + transcript, status = Open3.capture2e(*command, chdir: File.dirname(mpr)) + raise Failure, "mx check produced no JSON: #{transcript.lines.last(20).join.strip}" unless File.file?(output) + + diagnostics = DiagnosticSet.read(output) + unless status.exited? && status.exitstatus == diagnostics.exit_mask + raise Failure, + "mx check exit status #{status.exitstatus.inspect} does not match " \ + "diagnostic mask #{diagnostics.exit_mask}" + end + + diagnostics + end + end + + # Orchestrates validation, export/rebuild, asset, and optional oracle gates. + class Runner + def initialize(source, mxbuild: nil, mxcheck: nil, source_errors: nil, rebuilt_errors: nil, + strict_warnings: false) + @source = File.expand_path(source) + @mxbuild = mxbuild + @mxcheck = mxcheck + @source_errors = source_errors + @rebuilt_errors = rebuilt_errors + @strict_warnings = strict_warnings + paired_errors? + exclusive_oracle! + end + + def run + raise Failure, "MPR not found: #{@source}" unless File.file?(@source) + + source_validation = Mxrb.validate(@source) + raise Failure, "source MPR is invalid: #{source_validation.errors.join('; ')}" unless source_validation.valid? + + version = mendix_version(@source) + major = version.split('.').first.to_i + raise Failure, "unsupported Mendix generation: #{version}" unless [10, 11].include?(major) + + source_preflight = Mxrb.compatibility(@source) + + Dir.mktmpdir("mxrb-frontend-#{major}-") do |workspace| + report = perform_round_trip(workspace, version, source_preflight) + attach_oracle(report, workspace) + report[:scope] = report[:oracle] ? 'frontend' : 'round_trip' + report[:frontend_ready] = report.dig(:oracle, :frontend_ready) + report[:passed] = report.fetch(:gates).values.all? && report.dig(:assets, :passed) && + (report[:oracle].nil? || report.dig(:oracle, :passed)) + report + end + end + + private + + def paired_errors? + return true if @source_errors.nil? == @rebuilt_errors.nil? + + raise Failure, '--source-errors and --rebuilt-errors must be supplied together' + end + + def exclusive_oracle! + sources = [@mxbuild, @mxcheck, @source_errors].compact + return if sources.size <= 1 + + raise Failure, '--mxbuild, --mx, and reused error JSON are mutually exclusive' + end + + def mendix_version(path) + Mxrb.open(path, &:mendix_version) + end + + def perform_round_trip(workspace, version, source_preflight) + exported = File.join(workspace, 'ruby') + rebuilt_root = File.join(workspace, 'rebuilt') + rebuilt = File.join(rebuilt_root, File.basename(@source)) + FileUtils.mkdir_p(rebuilt_root) + Mxrb::Exporter.new(@source, exported).export! + evaluate_project(File.join(exported, 'project.rb'), rebuilt) + + validation = Mxrb.validate(rebuilt) + raise Failure, "rebuilt MPR is invalid: #{validation.errors.join('; ')}" unless validation.valid? + + preflight = Mxrb.compatibility(rebuilt) + comparison = Mxrb.compare(@source, rebuilt) + + { + passed: false, mendix_version: version, source: @source, + gates: { + source_valid: true, rebuilt_valid: true, source_preflight: source_preflight.compatible?, + rebuilt_preflight: preflight.compatible?, structural_round_trip: comparison.identical? + }, + preflight: { + source: preflight_summary(source_preflight), rebuilt: preflight_summary(preflight) + }, + structural_differences: comparison.differences.first(20), + assets: AssetGate.new(File.dirname(@source), exported, rebuilt_root).inspect, + rebuilt: File.basename(rebuilt), oracle: nil + } + end + + def preflight_summary(report) + { + compatible: report.compatible?, errors: report.errors.size, warnings: report.warnings.size, + categories: report.findings.group_by(&:category).transform_values { _1.sum(&:count) }.sort.to_h, + types: report.findings.group_by(&:type).transform_values { _1.sum(&:count) }.sort.to_h + } + end + + def evaluate_project(project, target) + previous = ENV['MXRB_OUTPUT_PATH'] + ENV['MXRB_OUTPUT_PATH'] = target + load project + ensure + previous.nil? ? ENV.delete('MXRB_OUTPUT_PATH') : ENV['MXRB_OUTPUT_PATH'] = previous + end + + def attach_oracle(report, workspace) + rebuilt = File.join(workspace, 'rebuilt', report.fetch(:rebuilt)) + source_set, rebuilt_set = diagnostic_sets(rebuilt, workspace) + return unless source_set + + require_observable_warnings!(source_set, rebuilt_set) + equivalent = source_set.equivalent?(rebuilt_set) + ready = source_set.ready?(strict_warnings: @strict_warnings) && + rebuilt_set.ready?(strict_warnings: @strict_warnings) + report[:oracle] = { + passed: equivalent && ready, equivalent:, frontend_ready: ready, + strict_warnings: @strict_warnings, source: source_set.to_h, rebuilt: rebuilt_set.to_h + } + end + + def require_observable_warnings!(*sets) + return unless @strict_warnings && sets.any? { !_1.warnings_observable? } + + raise Failure, + 'strict warning gate requires observable warnings; ' \ + 'MxBuild succeeded without producing --write-errors JSON' + end + + def diagnostic_sets(rebuilt, workspace) + return [DiagnosticSet.read(@source_errors), DiagnosticSet.read(@rebuilt_errors)] if @source_errors + + if @mxcheck + oracle = MxCheckOracle.new(@mxcheck) + return [ + oracle.diagnose(@source, File.join(workspace, 'mx-check-source')), + oracle.diagnose(rebuilt, File.join(workspace, 'mx-check-rebuilt')) + ] + end + return [nil, nil] unless @mxbuild + + oracle = MxBuildOracle.new(@mxbuild) + [ + oracle.diagnose(@source, File.join(workspace, 'mxbuild-source')), + oracle.diagnose(rebuilt, File.join(workspace, 'mxbuild-rebuilt')) + ] + end + end + + # Small executable boundary which always emits machine-readable evidence. + module CLI + module_function + + def run(argv) + options = { strict_warnings: false } + output = nil + parser = OptionParser.new do |cli| + cli.banner = 'Usage: script/frontend_acceptance APP.mpr [options]' + cli.on('--mxbuild FILE', 'Run MxBuild only as an external oracle') { options[:mxbuild] = _1 } + cli.on('--mx FILE', 'Run mx check as an observable diagnostic oracle') { options[:mxcheck] = _1 } + cli.on('--source-errors FILE', 'Reuse source MxBuild/mx check JSON') { options[:source_errors] = _1 } + cli.on('--rebuilt-errors FILE', 'Reuse rebuilt MxBuild/mx check JSON') { options[:rebuilt_errors] = _1 } + cli.on('--strict-warnings', 'Require zero MxBuild warnings too') { options[:strict_warnings] = true } + cli.on('-o', '--output FILE', 'Write JSON evidence to FILE') { options[:output] = _1 } + end + parser.parse!(argv) + source = argv.shift or raise OptionParser::MissingArgument, 'APP.mpr' + raise OptionParser::InvalidArgument, argv.join(' ') unless argv.empty? + + output = options.delete(:output) + report = Runner.new(source, **options).run + json = JSON.pretty_generate(report) + output ? File.write(output, "#{json}\n") : puts(json) + report.fetch(:passed) ? 0 : 1 + rescue Failure, OptionParser::ParseError => e + json = JSON.pretty_generate(passed: false, error: e.message) + output ? File.write(output, "#{json}\n") : warn(json) + 1 + end + end +end + +exit MxrbFrontendAcceptance::CLI.run(ARGV) if $PROGRAM_NAME == __FILE__ + +# rubocop:enable Metrics diff --git a/spec/access_rule_reference_spec.rb b/spec/access_rule_reference_spec.rb new file mode 100644 index 0000000..b5aeec1 --- /dev/null +++ b/spec/access_rule_reference_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'tmpdir' + +RSpec.describe 'qualified access-rule member references' do # rubocop:disable Metrics/BlockLength + it 'preserves inherited attribute ownership through export and rebuild' do + Dir.mktmpdir do |dir| + source = File.join(dir, 'source.mpr') + exported = File.join(dir, 'ruby') + rebuilt = File.join(dir, 'rebuilt.mpr') + Mxrb.define(source) do + mendix_version '11.12.1' + self.module :Catalog do + module_role :User + entity(:Base) { string :Name } + entity :Child do + generalizes 'Catalog.Base' + access_rule 'Catalog.User', default_rights: 'ReadWrite', members: [{ + name: 'Name', reference: 'Catalog.Base.Name', rights: 'ReadWrite', kind: :attribute + }] + end + end + end + + expect(member_reference(source)).to eq('Catalog.Base.Name') + Mxrb::Exporter.new(source, exported).export!(parallel: false) + child = File.read(File.join(exported, 'modules', 'Catalog', 'domain', 'entities', 'child.rb')) + expect(child).to include(':reference => "Catalog.Base.Name"') + with_output(rebuilt) { load File.join(exported, 'project.rb') } + expect(member_reference(rebuilt)).to eq('Catalog.Base.Name') + expect(Mxrb.validate(rebuilt)).to be_valid + end + end + + def member_reference(path) + Mxrb.open(path) do |project| + project.entities.find { _1.name == 'Child' }.access_rules.first.fetch(:members).first + .fetch(:reference) + end + end + + def with_output(path) + previous = ENV.fetch('MXRB_OUTPUT_PATH', nil) + ENV['MXRB_OUTPUT_PATH'] = path + yield + ensure + previous ? ENV['MXRB_OUTPUT_PATH'] = previous : ENV.delete('MXRB_OUTPUT_PATH') + end +end diff --git a/spec/combo_box_bundle_compiler_spec.rb b/spec/combo_box_bundle_compiler_spec.rb index 6bad54e..464372f 100644 --- a/spec/combo_box_bundle_compiler_spec.rb +++ b/spec/combo_box_bundle_compiler_spec.rb @@ -14,12 +14,13 @@ def property(id, primitive: '', **values) }.merge(values.transform_keys(&:to_s)) } end - def schema # rubocop:disable Metrics/MethodLength + def schema # rubocop:disable Metrics/AbcSize, Metrics/MethodLength types = [ property_type('source', 'source', 'Enumeration'), property_type('options-type', 'optionsSourceType', 'Enumeration'), property_type('caption-type', 'optionsSourceAssociationCaptionType', 'Enumeration'), property_type('association-caption', 'optionsSourceAssociationCaptionAttribute', 'Attribute'), + property_type('association-caption-expression', 'optionsSourceAssociationCaptionExpression', 'Expression'), property_type('association', 'attributeAssociation', 'Association'), property_type('association-source', 'optionsSourceAssociationDataSource', 'DataSource'), property_type('database-target', 'databaseAttributeString', 'Attribute'), @@ -179,7 +180,10 @@ def xpath(entity) expect(incomplete.send(:target_attribute)).to be_nil expect(incomplete.send(:database_caption_attribute)).to be_nil expect(incomplete.send(:database_value_attribute)).to be_nil + expect(incomplete.send(:enumeration_attribute)).to be_nil expect(incomplete.send(:association_caption_attribute)).to be_nil + expect(incomplete.send(:association_caption_expression)).to be_nil + expect(incomplete.send(:list_expression_property)).to include('"path": null') expect(incomplete.send(:entity_steps, nil)).to eq([]) expect(incomplete.send(:primitive, 'missing')).to be_nil expect(incomplete.send(:property_values, nil)).to eq({}) @@ -194,5 +198,51 @@ def xpath(entity) ] expect(compiler.send(:selection_property)).to include('Multiple') end + + it 'compiles a context enumeration without requiring a list data source' do + properties = [ + property('source', primitive: 'context'), property('options-type', primitive: 'enumeration'), + property('database-target', AttributeRef: nil), + property('association-caption', AttributeRef: nil), + property('clearable', primitive: 'true'), + { + 'TypePointer' => 'enum-target', + 'Value' => { 'AttributeRef' => { 'Attribute' => 'Demo.Order.Status' } } + } + ] + model = source + model.documents << property_type('enum-target', 'attributeEnumeration', 'Attribute') + compiler = described_class.new( + model, 'Demo.Edit', widget('status', properties), + scope: 'p.Demo.Edit.editor', entity: 'Demo.Order' + ) + + expect(compiler).to be_supported + expect(compiler.render).to include( + 'React.createElement($Combobox', '"optionsSourceType": "enumeration"', + '"attributeEnumeration": AttributeProperty', '"entity": "Demo.Order"', + '"attribute": "Status"' + ) + end + + it 'compiles a context association caption expression' do + properties = [ + property('source', primitive: 'context'), property('options-type', primitive: 'association'), + property('caption-type', primitive: 'expression'), + property('association-caption-expression', Expression: '$currentObject/Name'), + property('association', EntityRef: { 'Steps' => [2, { + 'Association' => 'Demo.Order_Location', 'DestinationEntity' => 'Demo.Location' + }] }), + property('association-source', DataSource: xpath('Demo.Location')) + ] + compiler = described_class.new( + source, 'Demo.Edit', widget('location', properties), scope: 'p.Demo.Edit.editor', entity: 'Demo.Order' + ) + + expect(compiler).to be_supported + expect(compiler.render).to include( + 'optionsSourceAssociationCaptionExpression', 'ListExpressionProperty', '"path": "Name"' + ) + end end # rubocop:enable Metrics/BlockLength diff --git a/spec/compatibility_analyzer_spec.rb b/spec/compatibility_analyzer_spec.rb index 974a7c3..673e4b1 100644 --- a/spec/compatibility_analyzer_spec.rb +++ b/spec/compatibility_analyzer_spec.rb @@ -48,7 +48,7 @@ def source(version:, units:, documents: []) it 'stops at an unsupported Mendix version instead of auditing it with the wrong renderer' do page = unit('Forms$Page', 'Home', 'Widgets' => [2, { '$Type' => 'Forms$UnknownWidget' }]) - report = described_class.new('/tmp/App.mpr', source: source(version: '10.18.0', units: [page])).analyze + report = described_class.new('/tmp/App.mpr', source: source(version: '8.18.0', units: [page])).analyze expect(report.errors.map(&:category)).to eq([:version]) end @@ -76,5 +76,68 @@ def source(version:, units:, documents: []) expect(report).to be_compatible expect(report.findings).to be_empty end + + it 'classifies generic and identified custom widget bundle findings' do + model = source(version: '11.12.1', units: []) + analyzer = described_class.new('/tmp/App.mpr', source: model) + bundle = double( + unsupported_widgets: ['CustomWidgets$CustomWidget', 'Forms$UnknownWidget'], + unsupported_custom_widgets: ['vendor.widget.Uncompiled'] + ) + + analyzer.send(:add_bundle_findings, bundle, 'Demo.Shell') + + expect(analyzer.send(:collapsed_findings).map(&:category)).to contain_exactly(:custom_widget, :widget) + end + + it 'classifies identified custom widgets found while compiling pages' do + page = unit('Forms$Page', 'Home') + model = source(version: '11.12.1', units: [page]) + bundle = double( + unsupported_widgets: ['CustomWidgets$CustomWidget'], + unsupported_custom_widgets: ['vendor.widget.Uncompiled'] + ) + compiler = instance_double(Mxrb::Compiler::PageBundleCompiler, compile: bundle) + allow(Mxrb::Compiler::PageBundleCompiler).to receive(:new).and_return(compiler) + + report = described_class.new('/tmp/App.mpr', source: model).analyze + + expect(report.errors.map(&:category)).to eq([:custom_widget]) + expect(report.errors.first.type).to eq('vendor.widget.Uncompiled') + end + + it 'turns legacy page, React page, and layout compiler failures into findings' do + page = unit('Forms$Page', 'Home') + layout = unit('Forms$Layout', 'Shell') + + legacy_model = source(version: '7.17.0', units: [page]) + legacy = described_class.new('/tmp/Legacy.mpr', source: legacy_model) + legacy.instance_variable_set(:@adapter, Mxrb::Compiler::Adapter.for('7.17.0')) + allow(Mxrb::Compiler::LegacyPageBuilder).to receive(:new).and_raise( + Mxrb::CompilationError, 'legacy failure' + ) + legacy.send(:analyze_legacy_pages) + expect(legacy.send(:collapsed_findings).last.message).to eq('legacy failure') + + modern_model = source(version: '11.12.1', units: [page, layout]) + modern = described_class.new('/tmp/Modern.mpr', source: modern_model) + allow(Mxrb::Compiler::PageBundleCompiler).to receive(:new).and_return( + double(compile: nil, compile_layout: nil) + ) + allow(Mxrb::Compiler::PageBundleCompiler).to receive(:new).and_raise( + Mxrb::CompilationError, 'React failure' + ) + modern.send(:analyze_pages) + modern.send(:analyze_layouts) + expect(modern.send(:collapsed_findings).map(&:message)) + .to include('React failure') + end + + it 'exposes compatibility through the public MXRB API' do + report = double + analyzer = instance_double(described_class, analyze: report) + allow(described_class).to receive(:new).with('/tmp/App.mpr').and_return(analyzer) + expect(Mxrb.compatibility('/tmp/App.mpr')).to equal(report) + end end # rubocop:enable Metrics/BlockLength diff --git a/spec/compiler_edge_coverage_spec.rb b/spec/compiler_edge_coverage_spec.rb index 3f6ff9a..a561715 100644 --- a/spec/compiler_edge_coverage_spec.rb +++ b/spec/compiler_edge_coverage_spec.rb @@ -137,6 +137,12 @@ def counterpart(_source) = nil bootstrap.instance_variable_set(:@source, Struct.new(:version).new('7.17.0')) expect(bootstrap.send(:web_template_roots)).to eq('web' => 'web') + classic_ten = Struct.new(:version, :optimized_web_client?).new('10.24.0', false) + bootstrap.instance_variable_set(:@source, classic_ten) + expect(bootstrap.send(:web_template_roots)).to eq('dojo-web' => 'web') + bootstrap.instance_variable_set(:@source, Struct.new(:version, :optimized_web_client?).new('10.24.0', true)) + expect(bootstrap.send(:web_template_roots)).to eq('react-web' => 'web') + source = Struct.new(:units) { def units_of(_type) = units }.new( [Mxrb::Compiler::SourceModel::Unit.new( id: 'images', container_id: 'images', containment: 'Images', module_name: '../bad', @@ -151,6 +157,60 @@ def counterpart(_source) = nil .to raise_error(Mxrb::CompilationError, /invalid Mendix version/) end + it 'selects the Mendix 10 Rollup command' do + web = Mxrb::Compiler::WebBundleBuilder.allocate + web.instance_variable_set(:@source, Struct.new(:version).new('10.24.0')) + web.instance_variable_set(:@version_root, '/mendix/10.24.0') + arguments = [ + '/mendix/10.24.0/modeler/tools/node/node_modules/rollup/dist/bin/rollup', '--config' + ] + + expect(web.send(:bundler_name)).to eq('Rollup') + expect(web.send(:bundler_arguments)).to eq(arguments) + end + + it 'covers the source-less Mendix 10 adapter and every React DOM shim decision' do + expect(Mxrb::Compiler::Adapter.for('10.24.0').web_profiles).to eq([:dojo]) + + Dir.mktmpdir do |root| + web = File.join(root, 'web') + FileUtils.mkdir_p(web) + builder = Mxrb::Compiler::WebBundleBuilder.allocate + builder.instance_variable_set(:@version_root, '/mendix/11.12.1') + + builder.send(:materialize_react_dom_compatibility, web) + shim = File.join(web, 'react-dom-compat.mjs') + expect(File).to exist(shim) + + config = File.join(web, 'rspack.config.mjs') + File.write(config, 'export default { resolve: { alias: { } } };') + builder.send(:materialize_react_dom_compatibility, web) + expect(File.read(config)).to include(JSON.generate(shim)) + builder.send(:materialize_react_dom_compatibility, web) + + File.write(config, 'export default {};') + expect { builder.send(:materialize_react_dom_compatibility, web) } + .to raise_error(Mxrb::CompilationError, /no resolve alias block/) + end + end + + it 'builds React layout bundles through the page bundle builder' do + Dir.mktmpdir do |root| + layout = Struct.new(:module_name, :document).new('Demo', { 'Name' => 'Shell' }) + source = instance_double(Mxrb::Compiler::SourceModel, units_of: [layout]) + allow(source).to receive(:web_layout?).with(layout).and_return(true) + bundle = Mxrb::Compiler::PageBundle.new('Demo.Shell', 'layout source', [], []) + compiler = instance_double(Mxrb::Compiler::PageBundleCompiler, compile_layout: bundle) + allow(Mxrb::Compiler::PageBundleCompiler).to receive(:new).with(source).and_return(compiler) + builder = Mxrb::Compiler::PageBundleBuilder.allocate + builder.instance_variable_set(:@source, source) + builder.instance_variable_set(:@web_root, root) + + expect(builder.send(:build_layouts)).to eq([bundle]) + expect(File.read(File.join(root, 'layouts', 'Demo.Shell.js'))).to eq('layout source') + end + end + it 'exercises all compatibility fallbacks that protect legacy build inputs' do values = Class.new { include Mxrb::Compiler::ModelValues }.new expect(values.send(:image_bytes, 'bytes')).to eq('bytes'.b) diff --git a/spec/compiler_spec.rb b/spec/compiler_spec.rb index 6a80daf..cc7503e 100644 --- a/spec/compiler_spec.rb +++ b/spec/compiler_spec.rb @@ -69,14 +69,46 @@ def create_runtime(root) expect(described_class::Adapter.for('6.10.8')).to be_a(described_class::Adapter::V6) expect(described_class::Adapter.for('7.2.3')).to be_a(described_class::Adapter::V7) expect(described_class::Adapter.for('9.24.0')).to be_a(described_class::Adapter::V9) + expect(described_class::Adapter.for('10.24.0')).to be_a(described_class::Adapter::V10) expect(described_class::Adapter.for('11.12.1')).to be_a(described_class::Adapter::V11) expect(described_class::Adapter.for('7.17.0').web_profiles).to eq([:dojo]) expect(described_class::Adapter.for('9.24.0').web_profiles).to eq(%i[dojo react_wrapper]) + classic = instance_double(described_class::SourceModel, optimized_web_client?: false) + optimized = instance_double(described_class::SourceModel, optimized_web_client?: true) + expect(described_class::Adapter.for('10.24.0', source: classic).web_profiles).to eq([:dojo]) + expect(described_class::Adapter.for('10.24.0', source: optimized).web_profiles).to eq([:react]) expect(described_class::Adapter.for('11.12.1').web_profiles).to eq([:react]) expect { described_class::Adapter.for('8.18.0') } - .to raise_error(Mxrb::UnsupportedVersion, /6\.x, 7\.x, 9\.x, and 11\.x/) - expect { described_class::Adapter.for('10.21.0') } - .to raise_error(Mxrb::UnsupportedVersion, /audited native compilation/) + .to raise_error(Mxrb::UnsupportedVersion, /6\.x, 7\.x, 9\.x, 10\.x, and 11\.x/) + end + + it 'reads the Mendix 10 optimized-client choice from project settings' do + source = described_class::SourceModel.allocate + allow(source).to receive(:documents).with('Settings$ProjectSettings').and_return([]) + expect(source.optimized_web_client?).to be(false) + + allow(source).to receive(:documents).with('Settings$ProjectSettings').and_return( + [{ + '$Type' => 'Settings$ProjectSettings', + 'Settings' => [ + 2, + { '$Type' => 'Forms$WebUIProjectSettingsPart', 'UseOptimizedClient' => 'Yes' } + ] + }] + ) + expect(source.optimized_web_client?).to be(true) + end + + it 'creates new Mendix 10 projects with the optimized client' do + Dir.mktmpdir do |root| + path = File.join(root, 'Optimized.mpr') + Mxrb.define(path) do + mendix_version '10.24.0.73019' + self.module(:App) + end + + expect(described_class::SourceModel.read(path).optimized_web_client?).to be(true) + end end it 'packs a deterministic MDA without invoking an external toolchain' do @@ -256,10 +288,12 @@ def create_runtime(root) expect(zip.read('etc/constants/variables.conf')).to include( '${?CONSTANTS_CLINIC_API_URL}' ) - expect(zip.read('etc/configurations/Default.conf')).to include('adminPassword = ""') + expect(zip.read('etc/configurations/Default.conf')).to include( + 'adminPassword = ""', 'DatabaseJdbcUrl = "jdbc:hsqldb:file:app/data/database/default"' + ) end expect(described_class::PortableConfiguration.new({}).files.fetch('etc/StudioPro.conf')) - .to include('ScheduledEventExecution = "NONE"') + .to include('ScheduledEventExecution = "NONE"', 'adminUser.password = ""') end end @@ -286,6 +320,26 @@ def create_runtime(root) end end + it 'packages runtimes that predate PAD using built-in portable assets' do + Dir.mktmpdir do |root| + mpr = create_project(root, version: '10.24.0.73019') + deployment = create_deployment(root, runtime_version: '10.24.0.73019') + runtime = create_runtime(root) + FileUtils.rm_rf(File.join(runtime, 'pad')) + output = File.join(root, 'runtime.zip') + + described_class::PortablePackager.new(mpr, deployment:, mendix_home: runtime).pack(output:) + + Zip::File.open(output) do |zip| + expect(zip.read('bin/start')).to include('#!/bin/sh', 'runtimelauncher.jar') + expect(zip.get_entry('bin/start').unix_perms).to eq(0o755) + expect(zip.read('etc/variables.conf')).to include('${?M2EE_ADMIN_PASS}') + expect(zip.read('etc/variables.conf')).to include('${?RUNTIME_ADMINUSER_PASSWORD}') + expect(zip.read('etc/example.conf')).to include('DatabaseType = HSQLDB') + end + end + end + it 'exposes portable packaging through the CLI' do Dir.mktmpdir do |root| mpr = create_project(root) diff --git a/spec/content_api_spec.rb b/spec/content_api_spec.rb index 7d4e0b4..b2fbcec 100644 --- a/spec/content_api_spec.rb +++ b/spec/content_api_spec.rb @@ -206,6 +206,7 @@ def query(url) expect { api.versions(170, version_id: 'bad') }.to raise_error(Mxrb::MarketplaceError, /version ID/) expect { api.versions(170, mendix_version: '11.next') } .to raise_error(Mxrb::MarketplaceError, /invalid Mendix/) + expect(api.send(:mendix_version_filter, '10.24.0.73019')).to eq('10.24.0') end end @@ -259,16 +260,22 @@ def query(url) .to raise_error(Mxrb::MarketplaceError, /requires --mpr/) end - it 'rejects non-module content when importing into an MPR' do + it 'accepts Widget metadata and defers trust to the downloaded package structure' do package = Mxrb::OfficialMarketplace::OfficialPackage.new( 'Widget', '1.0.0', :mendix, nil, 'content:10', 10, '123e4567-e89b-12d3-a456-426614174000', 'Widget', 'Regular', [], false, false ) - api = instance_double(Mxrb::OfficialMarketplace::ContentApi, resolve: package) - installer = described_class.new(target: Dir.pwd, mpr: __FILE__) - allow(installer).to receive(:detect_mendix_version).and_return('11.12.1') - expect { installer.pull_official('10', api:) } - .to raise_error(Mxrb::MarketplaceError, /not a Module/) + installer = described_class.new(target: Dir.pwd) + expect { installer.validate_official_package!(package) }.not_to raise_error + end + + it 'allows Marketplace Service content while the downloaded MPK validates the module boundary' do + package = Mxrb::OfficialMarketplace::OfficialPackage.new( + 'MQTT Connector', '2.2.1', :mendix, nil, 'content:119508', 119_508, + '123e4567-e89b-12d3-a456-426614174000', 'Service', 'Regular', [], false, false + ) + installer = described_class.new(target: Dir.pwd) + expect { installer.validate_official_package!(package) }.not_to raise_error end it 'closes safely when target Mendix version detection cannot open the MPR' do diff --git a/spec/frontend_acceptance_spec.rb b/spec/frontend_acceptance_spec.rb new file mode 100644 index 0000000..91e3b33 --- /dev/null +++ b/spec/frontend_acceptance_spec.rb @@ -0,0 +1,332 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength, Metrics/MethodLength + +require 'spec_helper' +require 'tmpdir' +load File.expand_path('../script/frontend_acceptance', __dir__) + +RSpec.describe MxrbFrontendAcceptance do + def problem(code:, severity: 'Error', message: 'problem', module_name: 'App', element: 'Widget') + { + 'severity' => severity, + 'errorCode' => code, + 'message' => message, + 'locations' => [{ + 'module' => module_name, 'document' => "Page 'Home'", 'element' => element, + 'unitId' => 'volatile-unit', 'elementId' => 'volatile-element' + }] + } + end + + def check_problem(code:, message: 'check problem') + { + 'code' => code, 'message' => message, + 'locations' => [{ + 'module-name' => 'App', 'document-name' => "Page 'Home'", + 'element-name' => 'Widget', 'unit-id' => 'unit', 'element-id' => 'element' + }] + } + end + + def check_payload(errors: [], warnings: [], deprecations: [], recommendations: []) + { + 'serialization_version' => 1, 'errors' => errors, 'warnings' => warnings, + 'deprecations' => deprecations, 'performance' => { 'recommendations' => recommendations }, + 'total-problems' => errors.size + warnings.size + deprecations.size + recommendations.size + } + end + + def errors_file(root, name, problems) + path = File.join(root, name) + File.write(path, JSON.generate('errors' => [], 'problems' => problems)) + path + end + + def project(root, version: '11.12.1', marketplace: false) + FileUtils.mkdir_p(File.join(root, 'theme', 'web')) + File.write(File.join(root, 'theme', 'web', 'main.scss'), '$brand: #123456;') + path = File.join(root, 'App.mpr') + Mxrb.define(path) do + mendix_version version + self.module(:App) { entity :Item } + end + return path unless marketplace + + FileUtils.mkdir_p(File.join(root, '.mxrb', 'marketplace')) + File.write(File.join(root, '.mxrb', 'marketplace.lock.json'), '{}') + File.binwrite(File.join(root, '.mxrb', 'marketplace', 'App-1.0.0.mpk'), 'package') + path + end + + it 'classifies MxBuild diagnostics and compares stable semantic signatures' do + source_payload = { + 'problems' => [problem(code: 'CE0462'), problem(code: 'CE9999', severity: 'Warning')] + } + rebuilt_payload = { + 'problems' => [problem(code: 'CE9999', severity: 'Warning'), problem(code: 'CE0462')] + } + source = described_class::DiagnosticSet.new(source_payload) + rebuilt = described_class::DiagnosticSet.new(rebuilt_payload) + + expect(source).to be_equivalent(rebuilt) + expect(source).not_to be_ready + expect(source.to_h).to include( + problems: 2, errors: 1, warnings: 1, + warnings_observable: true, + categories: { 'missing_widget_package' => 1, 'unclassified' => 1 }, + codes: { 'CE0462' => 1, 'CE9999' => 1 }, + modules: { 'App' => 2 }, elements: { 'Widget' => 2 } + ) + changed = described_class::DiagnosticSet.new( + { 'problems' => [problem(code: 'CE0462', message: 'different')] } + ) + expect(source).not_to be_equivalent(changed) + end + + it 'enforces optional warning strictness and validates diagnostic JSON fail-closed' do + warnings = described_class::DiagnosticSet.new( + { 'problems' => [problem(code: 'CW0263', severity: 'Warning')] } + ) + expect(warnings).to be_ready + expect(warnings.ready?(strict_warnings: true)).to be(false) + expect { described_class::DiagnosticSet.new([]) } + .to raise_error(described_class::Failure, /JSON object/) + expect { described_class::DiagnosticSet.new({ 'problems' => {} }) } + .to raise_error(described_class::Failure, /array/) + expect { described_class::DiagnosticSet.new({}) } + .to raise_error(described_class::Failure, /no problems array/) + expect do + described_class::DiagnosticSet.new( + { 'problems' => [problem(code: 'X', severity: 'Fatal')] } + ) + end.to raise_error(described_class::Failure, /unknown MxBuild severity/) + expect { described_class::DiagnosticSet.new({ 'problems' => ['bad'] }) } + .to raise_error(described_class::Failure, /must be an object/) + expect do + described_class::DiagnosticSet.new( + { 'problems' => [ + { 'severity' => 'Error', 'message' => 'bad', 'errorCode' => 'X', 'locations' => {} } + ] } + ) + end.to raise_error(described_class::Failure, /locations must be an array/) + end + + it 'normalizes mx check diagnostics and validates their exit-status mask' do + payload = check_payload( + errors: [check_problem(code: 'CE0001')], + warnings: [check_problem(code: 'CW0001')], + deprecations: [check_problem(code: 'DEPRECATED')], + recommendations: [check_problem(code: 'MXP011')] + ) + diagnostics = described_class::DiagnosticSet.new(payload) + expect(diagnostics.exit_mask).to eq(15) + expect(diagnostics.to_h).to include( + problems: 4, errors: 1, warnings: 3, warnings_observable: true, + kinds: { 'best_practice' => 1, 'deprecation' => 1, 'error' => 1, 'warning' => 1 } + ) + + expect do + described_class::DiagnosticSet.new(payload.merge('total-problems' => 3)) + end.to raise_error(described_class::Failure, /total-problems mismatch/) + expect do + described_class::DiagnosticSet.new(payload.merge('performance' => [])) + end.to raise_error(described_class::Failure, /performance must be an object/) + expect do + described_class::DiagnosticSet.new(payload.merge('warnings' => {})) + end.to raise_error(described_class::Failure, /warnings must be an array/) + end + + it 'fails closed on nonzero MxBuild exits without reported model errors' do + Dir.mktmpdir do |root| + mpr = project(File.join(root, 'source')) + executable = File.join(root, 'mxbuild') + File.write(executable, <<~RUBY) + #!/usr/bin/env ruby + require 'json' + errors = ARGV.find { _1.start_with?('--write-errors=') }.split('=', 2).last + File.write(errors, JSON.generate('problems' => [])) + exit 1 + RUBY + File.chmod(0o755, executable) + + oracle = described_class::MxBuildOracle.new(executable) + expect { oracle.diagnose(mpr, File.join(root, 'empty')) } + .to raise_error(described_class::Failure, /exited 1 without model errors/) + + File.write(executable, <<~RUBY) + #!/usr/bin/env ruby + require 'json' + errors = ARGV.find { _1.start_with?('--write-errors=') }.split('=', 2).last + problem = {'severity' => 'Error', 'errorCode' => 'CE0001', 'message' => 'model error'} + File.write(errors, JSON.generate('problems' => [problem])) + exit 1 + RUBY + result = oracle.diagnose(mpr, File.join(root, 'model-error')) + expect(result).not_to be_ready + expect { described_class::MxBuildOracle.new(File.join(root, 'missing')) } + .to raise_error(described_class::Failure, /not executable/) + end + end + + it 'proves zero errors without inventing warnings when live MxBuild omits diagnostics' do + Dir.mktmpdir do |root| + source = project(File.join(root, 'source')) + executable = File.join(root, 'mxbuild') + File.write(executable, <<~RUBY) + #!/usr/bin/env ruby + output = ARGV.find { _1.start_with?('--output=') }.split('=', 2).last + File.binwrite(output, 'mda') + exit 0 + RUBY + File.chmod(0o755, executable) + + oracle = described_class::MxBuildOracle.new(executable) + diagnostics = oracle.diagnose(source, File.join(root, 'oracle')) + expect(diagnostics).to be_ready + expect(diagnostics.ready?(strict_warnings: true)).to be(false) + expect(diagnostics.to_h).to include( + problems: nil, errors: 0, warnings: nil, warnings_observable: false + ) + + report = described_class::Runner.new(source, mxbuild: executable).run + expect(report).to include(passed: true, scope: 'frontend', frontend_ready: true) + expect(report.dig(:oracle, :source)).to include( + errors: 0, warnings: nil, warnings_observable: false + ) + + expect do + described_class::Runner.new(source, mxbuild: executable, strict_warnings: true).run + end.to raise_error(described_class::Failure, /strict warning gate requires observable warnings/) + end + end + + it 'runs mx check with observable diagnostics and rejects tool/status mismatches' do + Dir.mktmpdir do |root| + source = project(File.join(root, 'source')) + executable = File.join(root, 'mx') + payload = check_payload( + warnings: [check_problem(code: 'CW0055')], + recommendations: [check_problem(code: 'MXP011')] + ) + File.write(executable, <<~RUBY) + #!/usr/bin/env ruby + require 'json' + output = ARGV.fetch(ARGV.index('--json') + 1) + File.write(output, #{JSON.generate(payload).inspect}) + exit 10 + RUBY + File.chmod(0o755, executable) + + report = described_class::Runner.new(source, mxcheck: executable).run + expect(report).to include(passed: true, scope: 'frontend', frontend_ready: true) + expect(report.dig(:oracle, :source)).to include( + errors: 0, warnings: 2, warnings_observable: true, + kinds: { 'best_practice' => 1, 'warning' => 1 } + ) + strict = described_class::Runner.new(source, mxcheck: executable, strict_warnings: true).run + expect(strict).to include(passed: false, frontend_ready: false) + + File.write(executable, <<~RUBY) + #!/usr/bin/env ruby + require 'json' + output = ARGV.fetch(ARGV.index('--json') + 1) + File.write(output, #{JSON.generate(payload).inspect}) + exit 0 + RUBY + oracle = described_class::MxCheckOracle.new(executable) + expect { oracle.diagnose(source, File.join(root, 'mismatch')) } + .to raise_error(described_class::Failure, /does not match diagnostic mask 10/) + + File.write(executable, "#!/usr/bin/env ruby\nwarn 'broken'\nexit 1\n") + expect { oracle.diagnose(source, File.join(root, 'missing-json')) } + .to raise_error(described_class::Failure, /produced no JSON: broken/) + expect { described_class::MxCheckOracle.new(File.join(root, 'missing')) } + .to raise_error(described_class::Failure, /mx is not executable/) + end + end + + it 'round-trips Mendix 10 and 11 with exact assets and native preflight' do + Dir.mktmpdir do |root| + %w[10.24.0.73019 11.12.1].each do |version| + source_root = File.join(root, version) + FileUtils.mkdir_p(source_root) + report = described_class::Runner.new(project(source_root, version:)).run + + expect(report).to include( + passed: true, mendix_version: version, oracle: nil, + scope: 'round_trip', frontend_ready: nil + ) + expect(report[:gates].values).to all(be(true)) + expect(report[:assets]).to include(passed: true, files: 1, bytes: 16) + end + end + end + + it 'keeps round-trip equivalence separate from frontend readiness' do + Dir.mktmpdir do |root| + source = project(File.join(root, 'source')) + errors = [problem(code: 'CE0463')] + source_errors = errors_file(root, 'source.json', errors) + rebuilt_errors = errors_file(root, 'rebuilt.json', errors) + report = described_class::Runner.new(source, source_errors:, rebuilt_errors:).run + + expect(report[:passed]).to be(false) + expect(report).to include(scope: 'frontend', frontend_ready: false) + expect(report[:oracle]).to include(passed: false, equivalent: true, frontend_ready: false) + expect(report.dig(:oracle, :source, :categories)).to eq('stale_widget_definition' => 1) + end + end + + it 'rejects diagnostic divergence, unpaired evidence, and unsupported generations' do + Dir.mktmpdir do |root| + source = project(File.join(root, 'source')) + source_errors = errors_file(root, 'source.json', []) + rebuilt_errors = errors_file(root, 'rebuilt.json', [problem(code: 'CE6087')]) + report = described_class::Runner.new(source, source_errors:, rebuilt_errors:).run + expect(report.dig(:oracle, :equivalent)).to be(false) + + expect { described_class::Runner.new(source, source_errors:) } + .to raise_error(described_class::Failure, /supplied together/) + expect { described_class::Runner.new(source, mxcheck: '/mx', mxbuild: '/mxbuild') } + .to raise_error(described_class::Failure, /mutually exclusive/) + old = project(File.join(root, 'old'), version: '9.6.1.29396') + expect { described_class::Runner.new(old).run } + .to raise_error(described_class::Failure, /unsupported Mendix generation/) + end + end + + it 'preserves Marketplace lifecycle provenance as part of the no-loss asset gate' do + Dir.mktmpdir do |root| + source = project(File.join(root, 'source'), marketplace: true) + originals = File.join(root, 'source', '.mxrb', 'marketplace-originals') + FileUtils.mkdir_p(originals) + File.binwrite(File.join(originals, 'Widget.mpk'), 'original') + report = described_class::Runner.new(source).run + + expect(report[:passed]).to be(true) + expect(report.dig(:assets, :marketplace_provenance)).to eq( + files: 3, missing: [], changed: [], unexpected: [] + ) + end + end + + it 'rejects symlinks anywhere inside the no-loss asset boundary' do + Dir.mktmpdir do |root| + source = File.join(root, 'source') + exported = File.join(root, 'exported') + rebuilt = File.join(root, 'rebuilt') + [source, rebuilt, File.join(exported, '.mxrb')].each { FileUtils.mkdir_p(_1) } + target = File.join(root, 'lock.json') + File.write(target, '{}') + FileUtils.mkdir_p(File.join(source, '.mxrb')) + File.symlink(target, File.join(source, '.mxrb', 'marketplace.lock.json')) + File.write(File.join(exported, '.mxrb', 'assets.json'), JSON.generate('version' => 1, 'files' => [])) + + gate = described_class::AssetGate.new(source, exported, rebuilt) + expect { gate.inspect }.to raise_error(described_class::Failure, /contains symlinks/) + end + end +end + +# rubocop:enable Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/frontend_migrator_cli_spec.rb b/spec/frontend_migrator_cli_spec.rb new file mode 100644 index 0000000..3ef359b --- /dev/null +++ b/spec/frontend_migrator_cli_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'open3' +require 'tmpdir' + +# rubocop:disable Metrics/BlockLength, Metrics/MethodLength +RSpec.describe 'frontend migrator CLI' do + def executable + [RbConfig.ruby, File.expand_path('../bin/mxrb', __dir__), 'frontend'] + end + + def project(dir, version: '10.24.0.73019', layout: true) + path = File.join(dir, 'App.mpr') + Mxrb.define(path) do + mendix_version version + self.module(:App) { entity :Item } + end + return path unless layout + + mpr = Mxrb::IO::MprFile.open(path) + mpr.insert_unit( + container_uuid: mpr.root_unit.fetch('UnitID'), containment_name: 'Documents', + contents_doc: { + '$Type' => 'Forms$Page', 'Name' => 'Home', + 'Row' => { + '$Type' => 'Forms$LayoutGridRow', + 'Columns' => [{ '$Type' => 'Forms$LayoutGridColumn', 'Weight' => -1 }] + } + } + ) + path + ensure + mpr&.close + end + + def row_weight(path) + Mxrb.open(path) do |model| + page = model.all_units.find { model.parse_bson(_1)['$Type'] == 'Forms$Page' } + model.parse_bson(page).dig('Row', 'Columns', 0, 'Weight') + end + end + + it 'previews and applies a safe migration with the complete JSON contract' do + Dir.mktmpdir do |dir| + path = project(dir) + stdout, stderr, status = Open3.capture3(*executable, 'migrate', path, '--json') + payload = JSON.parse(stdout) + expect(status).to be_success + expect(stderr).to be_empty + expect(payload).to eq( + 'version' => '10.24.0.73019', 'changes' => 1, 'widgets' => 0, + 'layout_rows' => 1, 'design_properties' => 0, 'issues' => [], + 'safe' => true, 'applied' => false + ) + expect(row_weight(path)).to eq(-1) + + stdout, stderr, status = Open3.capture3(*executable, 'migrate', path, '--apply', '--json') + expect(status).to be_success + expect(stderr).to be_empty + expect(JSON.parse(stdout)).to include('safe' => true, 'applied' => true, 'layout_rows' => 1) + expect(row_weight(path)).to eq(12) + end + end + + it 'renders a human preview and exits nonzero with structured issues when blocked' do + Dir.mktmpdir do |dir| + safe_path = project(dir) + stdout, stderr, status = Open3.capture3(*executable, 'migrate', safe_path) + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout).to include( + 'Version : 10.24.0.73019', 'Changes : 1', + 'Layout rows : 1', 'Safe : true', 'Applied : false' + ) + + blocked = project(File.join(dir, 'blocked'), version: '9.24.0', layout: false) + stdout, stderr, status = Open3.capture3(*executable, 'migrate', blocked, '--apply', '--json') + payload = JSON.parse(stdout) + expect(status.exitstatus).to eq(1) + expect(stderr).to be_empty + expect(payload).to include( + 'version' => '9.24.0', 'changes' => 0, 'safe' => false, 'applied' => false + ) + expect(payload.fetch('issues')).to include(include('kind' => 'unsupported_version')) + end + end + + it 'documents usage and rejects missing or unknown actions and options' do + stdout, stderr, status = Open3.capture3(*executable, '--help') + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout).to include('mxrb frontend migrate FILE.mpr', '--apply', '--json') + + stdout, stderr, status = Open3.capture3(*executable, 'migrate', '--help') + expect(status).to be_success + expect(stderr).to be_empty + expect(stdout).to include('changed only when --apply') + + _stdout, stderr, status = Open3.capture3(*executable, 'unknown') + expect(status).not_to be_success + expect(stderr).to include('Unknown frontend action') + + _stdout, stderr, status = Open3.capture3(*executable, 'migrate') + expect(status).not_to be_success + expect(stderr).to include('Usage: mxrb frontend migrate') + + Dir.mktmpdir do |dir| + path = project(dir, layout: false) + _stdout, stderr, status = Open3.capture3(*executable, 'migrate', path, '--unknown') + expect(status).not_to be_success + expect(stderr).to include('Unknown arguments: --unknown') + end + end +end +# rubocop:enable Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/frontend_migrator_edge_spec.rb b/spec/frontend_migrator_edge_spec.rb new file mode 100644 index 0000000..a394cca --- /dev/null +++ b/spec/frontend_migrator_edge_spec.rb @@ -0,0 +1,469 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'zip' + +# rubocop:disable Metrics/BlockLength +RSpec.describe Mxrb::Frontend::Migrator do + subject(:migrator) { described_class.new('/tmp/App.mpr') } + + def array(items = [], marker: 2) + Mxrb::IO::BsonCodec.build_array(items, marker:) + end + + def value_type(type, default: '', required: false, object_type: nil, selections: []) + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetValueType', + 'Type' => type, 'DefaultValue' => default, 'Required' => required, + 'Translations' => array, 'SelectionTypes' => array(selections, marker: 1), + 'ObjectType' => object_type + } + end + + def property_type(key, value_type) + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetPropertyType', + 'PropertyKey' => key, 'ValueType' => value_type + } + end + + def object_type(types) + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetObjectType', + 'PropertyTypes' => array(types) + } + end + + def property(type, value) + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetProperty', + 'TypePointer' => type['$ID'], 'Value' => value + } + end + + def object(type, properties) + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$WidgetObject', + 'Properties' => array(properties), 'TypePointer' => type['$ID'] + } + end + + def default_value(type) + migrator.send(:default_value, type) + end + + it 'covers fail-closed envelopes and schema rebinding edge contracts' do + issues = [] + expect(migrator.send(:safe_envelope?, {}, {}, {}, 'u', '$', issues)).to be(false) + + widget = { 'Type' => { 'Mystery' => true }, 'Object' => {} } + expect(migrator.send(:safe_envelope?, widget, {}, {}, 'u', '$', issues)).to be(false) + widget = { 'Type' => {}, 'Object' => { 'Mystery' => true } } + expect(migrator.send(:safe_envelope?, widget, {}, {}, 'u', '$', issues)).to be(false) + expect(migrator.send(:safe_envelope?, { 'Type' => {}, 'Object' => {} }, {}, {}, + 'u', '$', issues)).to be(true) + + old_vt = value_type('String') + old_pt = property_type('legacy', old_vt) + old_type = object_type([old_pt]) + new_type = object_type([]) + old_value = default_value(old_vt) + old_object = object(old_type, [property(old_pt, old_value)]) + new_object = object(new_type, []) + expect(migrator.send(:rebind_object, old_object, old_type, new_object, new_type, + 'u', '$', issues)).to be_a(Hash) + + old_object['Properties'] = array([{ 'TypePointer' => SecureRandom.uuid, 'Value' => old_value }]) + expect(migrator.send(:rebind_object, old_object, old_type, new_object, new_type, + 'u', '$', issues)).to be(false) + expect(issues.map(&:kind)).to include(:malformed_widget, :unknown_widget_schema, + :unknown_widget_object, :unknown_property_pointer) + end + + it 'covers widget values, conversions, objects and inactive captions' do + issues = [] + string = value_type('String') + expect(migrator.send(:rebind_value, nil, string, string, 'u', '$', issues)).to be(false) + + nested_old = value_type('Object', object_type: nil) + nested_new = value_type('Object', object_type: nil) + empty = default_value(nested_old) + rebound = migrator.send(:rebind_value, empty, nested_old, nested_new, 'u', '$', issues) + expect(Mxrb::IO::BsonCodec.parse_array(rebound['Objects'])[:items]).to be_empty + + empty['Objects'] = array([{ '$Type' => 'CustomWidgets$WidgetObject' }]) + expect(migrator.send(:rebind_value, empty, nested_old, nested_new, 'u', '$', issues)).to be(false) + + boolean = value_type('Boolean', default: 'false') + expression = value_type('Expression') + optional_text = value_type('TextTemplate') + expect(default_value(optional_text)['TextTemplate']).to be_nil + explicit_selection = value_type('Selection', default: 'Multi', selections: %w[Single Multi]) + expect(default_value(explicit_selection)['Selection']).to eq('Multi') + invalid = default_value(boolean).merge('PrimitiveValue' => 'maybe') + expect(migrator.send(:convert_value, invalid, boolean, expression, 'u', '$', issues)).to be(false) + configured = default_value(boolean).merge('PrimitiveValue' => 'true', 'Image' => 'configured') + expect(migrator.send(:convert_value, configured, boolean, expression, + 'u', '$', issues)).to be(false) + + caption_type = property_type('selectCaption', value_type('TextTemplate', required: true)) + controller_type = property_type('select', value_type('Boolean', default: 'false')) + caption = property(caption_type, default_value(caption_type['ValueType'])) + controller = property(controller_type, default_value(controller_type['ValueType'])) + properties = { 'selectCaption' => caption, 'select' => controller } + migrator.send(:clear_inactive_default_captions!, properties, + 'selectCaption' => caption_type, 'select' => controller_type) + expect(caption.dig('Value', 'TextTemplate')).to be_nil + + custom = property(caption_type, default_value(caption_type['ValueType'])) + custom['Value']['PrimitiveValue'] = 'custom' + migrator.send(:clear_inactive_default_captions!, { 'selectCaption' => custom }, + 'selectCaption' => caption_type) + expect(custom.dig('Value', 'TextTemplate')).not_to be_nil + end + + it 'covers weight normalization and design-property helpers' do + expect(migrator.send(:normalize_weights, [1, 11])).to be_nil + expect(migrator.send(:normalize_weights, ['x', -1])).to be_nil + expect(migrator.send(:normalize_weights, [12, -1])).to be_nil + expect(migrator.send(:normalize_weights, [13])).to be_nil + expect(migrator.send(:normalize_weights, [10, -1, -1, -1])).to be_nil + expect(migrator.send(:normalize_weights, [-1, -1])).to eq([6, 6]) + + mappings = {} + migrator.send(:collect_design_aliases, nil, mappings) + property = { + 'name' => 'New', 'type' => 'Dropdown', 'oldNames' => ['Old'], + 'options' => [{ 'name' => 'On', 'oldNames' => ['Yes'] }] + } + migrator.send(:collect_design_aliases, property, mappings) + migrator.send(:add_design_alias, mappings, ['Old', nil], key: 'Conflict') + expect(mappings.values).to include(:conflict) + + spacing = { + 'name' => 'Spacing', 'type' => 'Spacing', + 'margin' => [{ 'name' => 'M', 'top' => { 'oldNames' => ['Old top'] } }] + } + spacing_mappings = {} + migrator.send(:collect_spacing_mappings, nil, spacing_mappings) + migrator.send(:collect_spacing_mappings, spacing, spacing_mappings) + migrator.send(:collect_spacing_mappings, spacing, spacing_mappings) + expect(spacing_mappings['Old top']).to include(key: 'Spacing', child: 'margin-top') + expect(migrator.send(:design_property_name, nil)).to be_nil + expect(migrator.send(:compound_design_property?, nil)).to be(false) + end + + it 'covers audited package visibility and model evolution normalization' do + boolean = value_type('Boolean') + enum = value_type('Enumeration') + selection = value_type('Selection', selections: %w[Single Multi]) + text = value_type('TextTemplate', required: true) + nested_types = %w[showContentAs dynamicText exportValue tooltip].map do |key| + property_type(key, key == 'showContentAs' ? enum : text) + end + nested_object_type = object_type(nested_types) + columns_type = property_type('columns', value_type('Object', object_type: nested_object_type)) + top_types = [ + columns_type, property_type('pagination', enum), property_type('itemSelection', selection), + property_type('loadMoreButtonCaption', text), + property_type('clearSelectionButtonLabel', text), + property_type('singleSelectionColumnLabel', text), property_type('orphan', boolean) + ] + top_type = object_type(top_types) + column_values = nested_types.map do |type| + value = default_value(type['ValueType']) + value['PrimitiveValue'] = 'attribute' if type['PropertyKey'] == 'showContentAs' + property(type, value) + end + column_values << { 'TypePointer' => SecureRandom.uuid, 'Value' => {} } + column = object(nested_object_type, column_values) + properties = top_types.map do |type| + value = default_value(type['ValueType']) + value['Objects'] = array([column]) if type['PropertyKey'] == 'columns' + property(type, value) + end + properties << { 'TypePointer' => SecureRandom.uuid, 'Value' => {} } + top_object = object(top_type, properties) + + migrator.send(:apply_audited_package_migration!, top_object, + { 'ObjectType' => top_type }, 'other') + migrator.send(:apply_audited_package_migration!, top_object, + { 'ObjectType' => top_type }, described_class::AUDITED_DATA_GRID_PACKAGE_SHA256) + expect(properties[3].dig('Value', 'TextTemplate')).to be_nil + + migrator.send(:clear_audited_data_grid_column_texts!, nil, nil) + page_variable = { + '$Type' => 'Forms$PageVariable', 'SnippetParameter' => 'P', 'Other' => [{}] + } + expect(migrator.send(:normalize_source_variable, page_variable)['SubKey']).to eq('') + nanoflow = { '$Type' => 'Forms$CallNanoflowClientAction', 'Nanoflow' => 'N' } + microflow = { '$Type' => 'Forms$MicroflowSettings', 'Microflow' => 'M' } + expect(migrator.send(:normalize_source_variable, nanoflow)).to have_key('OutputMappings') + expect(migrator.send(:normalize_source_variable, microflow)).to have_key('OutputMappings') + expect(migrator.send(:normalize_source_variable, [1, 'x'])).to eq([1, 'x']) + end + + it 'covers id preservation and canonical copying across nonmatching shapes' do + target = { '$ID' => 'new', 'Child' => [{ '$ID' => 'new-child' }], 'Only' => true } + source = { '$ID' => 'old', 'Child' => [{ '$ID' => 'old-child' }] } + expect(migrator.send(:preserve_ids!, target, source).dig('Child', 0, '$ID')).to eq('old-child') + expect(migrator.send(:preserve_ids!, {}, [])).to eq({}) + expect(migrator.send(:preserve_ids!, [], {})).to eq([]) + expect(migrator.send(:canonical_schema, [{ '$ID' => 'x', 'Value' => 1 }])) + .to eq([{ 'Value' => 1 }]) + expect(migrator.send(:deep_copy, 1)).to eq(1) + end + + it 'covers migration-plan concurrency guards and widget early exits' do + plan = Mxrb::Frontend::MigrationPlan.new(path: '/tmp/missing.mpr', version: '11', + changes: [], issues: []) + allow(Mxrb::IO::MprFile).to receive(:open).and_raise(Errno::ENOENT) + expect { plan.apply! }.to raise_error(Errno::ENOENT) + + fake_mpr = instance_double(Mxrb::IO::MprFile, close: nil) + allow(Mxrb::IO::MprFile).to receive(:open).and_return(fake_mpr) + allow(fake_mpr).to receive(:transaction).and_yield + allow(fake_mpr).to receive(:unit).and_return(nil) + change = Mxrb::Frontend::MigrationChange.new('gone', 'hash', {}, {}, 0, 0, 0) + gone = Mxrb::Frontend::MigrationPlan.new(path: '/tmp/x.mpr', version: '11', + changes: [change], issues: []) + expect { gone.apply! }.to raise_error(Mxrb::SerializationError, /disappeared/) + allow(fake_mpr).to receive(:unit).and_return('ContentsHash' => 'other') + expect { gone.apply! }.to raise_error(Mxrb::SerializationError, /changed after preview/) + + counts = { widgets: 0 } + issues = [] + migrator.send(:migrate_widget!, { 'Type' => {} }, 'u', '$', issues, counts) + allow(migrator).to receive(:definition).and_return(nil) + migrator.send(:migrate_widget!, { 'Type' => { 'WidgetId' => 'missing' } }, + 'u', '$', issues, counts) + allow(migrator).to receive(:definition).and_call_original + end + + it 'covers nested object success and value rejection branches' do + old_child_value = value_type('String') + old_child_property = property_type('name', old_child_value) + old_child_type = object_type([old_child_property]) + new_child_value = value_type('String') + new_child_property = property_type('name', new_child_value) + new_child_type = object_type([new_child_property]) + old_outer = value_type('Object', object_type: old_child_type) + new_outer = value_type('Object', object_type: new_child_type) + child = object(old_child_type, [property(old_child_property, default_value(old_child_value))]) + outer = default_value(old_outer) + outer['Objects'] = array([child]) + issues = [] + result = migrator.send(:rebind_value, outer, old_outer, new_outer, 'u', '$', issues) + expect(Mxrb::IO::BsonCodec.parse_array(result['Objects'])[:items].size).to eq(1) + + incompatible = value_type('Integer') + old_pt = property_type('name', old_child_value) + new_pt = property_type('name', incompatible) + expect(migrator.send(:rebind_object, + object(old_child_type, [property(old_pt, default_value(old_child_value))]), + object_type([old_pt]), object(object_type([new_pt]), []), + object_type([new_pt]), 'u', '$', issues)).to be_nil + + no_id = object(object_type([]), []) + no_id.delete('$ID') + expect(migrator.send(:rebind_object, no_id, object_type([]), object(object_type([]), []), + object_type([]), 'u', '$', issues)).to be_a(Hash) + expect(migrator.send(:object_template, new_child_type)).to include('$Type' => 'CustomWidgets$WidgetObject') + end + + it 'covers layout and design no-op, conflict, cache and malformed-file paths' do + counts = { layout_rows: 0, design_properties: 0 } + issues = [] + migrator.send(:migrate_layout_row!, { 'Columns' => array }, 'u', '$', issues, counts) + valid_columns = array([{ 'Weight' => 12 }]) + migrator.send(:migrate_layout_row!, { 'Columns' => valid_columns }, 'u', '$', issues, counts) + expect(counts[:layout_rows]).to eq(0) + + migrator.instance_variable_set(:@spacing_mappings, {}) + migrator.instance_variable_set(:@design_alias_mappings, {}) + migrator.send(:migrate_design_properties!, { 'DesignProperties' => array }, + 'u', '$', issues, counts) + plain = { + '$Type' => 'Forms$DesignPropertyValue', 'Key' => 'Plain', + 'Value' => { '$Type' => 'Forms$OptionDesignPropertyValue', 'Option' => 'On' } + } + owner = { 'DesignProperties' => array([plain]) } + migrator.send(:migrate_design_properties!, owner, 'u', '$', issues, counts) + expect(counts[:design_properties]).to eq(0) + + migrator.instance_variable_set(:@spacing_mappings, 'Old::M' => { + key: 'Spacing', child: 'margin-top', option: 'M' + }) + existing = migrator.send(:compound_design_property, 'Spacing') + existing['Value']['Properties'] = array([ + migrator.send(:option_design_property, 'margin-top', 'L') + ]) + legacy = Marshal.load(Marshal.dump(plain)).merge('Key' => 'Old') + legacy['Value']['Option'] = 'M' + owner = { 'DesignProperties' => array([existing, legacy]) } + migrator.send(:migrate_design_properties!, owner, 'u', '$', issues, counts) + expect(issues.map(&:kind)).to include(:conflicting_design_property) + expect(migrator.send(:spacing_mappings)).to be_a(Hash) + expect(migrator.send(:design_alias_mappings)).to be_a(Hash) + end + + it 'covers definition cache, nil-safe accessors and all audited visibility outcomes' do + migrator.instance_variable_set(:@definitions, 'cached' => :definition) + expect(migrator.send(:definition, 'cached')).to eq(:definition) + expect(migrator.send(:definition, 'missing')).to be_nil + expect(migrator.send(:definition, 'missing')).to be_nil + expect(migrator.send(:package_digest, 'missing')).to be_nil + expect(migrator.send(:property_types, nil)).to eq([]) + expect(migrator.send(:property_values, nil)).to eq([]) + + enum = value_type('Enumeration') + selection = value_type('Selection', selections: %w[Single Multi]) + text = value_type('TextTemplate', required: true) + nested_types = %w[showContentAs dynamicText exportValue tooltip].map do |key| + property_type(key, key == 'showContentAs' ? enum : text) + end + nested_schema = object_type(nested_types) + columns = property_type('columns', value_type('Object', object_type: nested_schema)) + top_types = [columns, property_type('pagination', enum), + property_type('itemSelection', selection), + property_type('loadMoreButtonCaption', text), + property_type('clearSelectionButtonLabel', text), + property_type('singleSelectionColumnLabel', text)] + top_schema = object_type(top_types) + column_objects = %w[dynamicText customContent].map do |content| + props = nested_types.map do |type| + value = default_value(type['ValueType']) + value['PrimitiveValue'] = content if type['PropertyKey'] == 'showContentAs' + property(type, value) + end + object(nested_schema, props) + end + top_properties = top_types.map do |type| + value = default_value(type['ValueType']) + case type['PropertyKey'] + when 'columns' then value['Objects'] = array(column_objects) + when 'pagination' then value['PrimitiveValue'] = 'loadMore' + when 'itemSelection' then value['Selection'] = 'Multi' + end + property(type, value) + end + top_object = object(top_schema, top_properties) + migrator.send(:apply_audited_package_migration!, top_object, { 'ObjectType' => top_schema }, + described_class::AUDITED_DATA_GRID_PACKAGE_SHA256) + expect(top_object).to be_a(Hash) + migrator.send(:clear_text_template!, nil) + end + + it 'closes the remaining guards, design merges and malformed package branches' do + allow(Mxrb::IO::MprFile).to receive(:open).and_raise(Errno::ENOENT) + expect { described_class.new('/tmp/absent.mpr').plan }.to raise_error(Errno::ENOENT) + + fake_mpr = instance_double(Mxrb::IO::MprFile, close: nil) + allow(Mxrb::IO::MprFile).to receive(:open).and_return(fake_mpr) + allow(fake_mpr).to receive(:transaction).and_yield + settled = Mxrb::Frontend::MigrationPlan.new(path: '/tmp/x.mpr', version: '11', + changes: [], issues: []) + expect(settled.apply!).to equal(settled) + expect { settled.apply! }.to raise_error(Mxrb::SerializationError, /already applied/) + + issues = [] + counts = { widgets: 0 } + definition = instance_double(Mxrb::WidgetPackage::Definition) + allow(migrator).to receive(:definition).and_return(definition) + empty_type = object_type([]) + template = [{ 'WidgetId' => 'x', 'ObjectType' => empty_type }, object(empty_type, [])] + allow(Mxrb::WidgetPackage).to receive(:template).and_return(template) + malformed = { '$Type' => 'CustomWidgets$CustomWidget', 'Type' => { 'WidgetId' => 'x' } } + migrator.send(:migrate_widget!, malformed, 'u', '$', issues, counts) + expect(issues.map(&:kind)).to include(:malformed_widget) + + string = value_type('String') + string['Translations'] = array([{ 'LanguageCode' => 'en_US', 'Text' => 'Default' }]) + expect(default_value(string)).to be_a(Hash) + expect(migrator.send(:normalized_value, {}, string)).to include('PrimitiveValue' => '') + boolean = value_type('Boolean') + expression = value_type('Expression') + expect(migrator.send(:convert_value, nil, boolean, expression, 'u', '$', issues)).to be(false) + + old_nested_property = property_type('changed', string) + new_nested_property = property_type('changed', value_type('Integer')) + old_nested_type = object_type([old_nested_property]) + new_nested_type = object_type([new_nested_property]) + old_outer = value_type('Object', object_type: old_nested_type) + new_outer = value_type('Object', object_type: new_nested_type) + configured = default_value(string) + configured['PrimitiveValue'] = 'kept' + nested = object(old_nested_type, [property(old_nested_property, configured)]) + old_outer_value = default_value(old_outer) + old_outer_value['Objects'] = array([nested]) + expect(migrator.send(:rebind_value, old_outer_value, old_outer, new_outer, + 'u', '$', issues)).to be_nil + + caption_type = property_type('goCaption', value_type('TextTemplate', required: true)) + controller_type = property_type('go', value_type('Boolean', default: 'false')) + caption = property(caption_type, default_value(caption_type['ValueType'])) + caption['Value']['PrimitiveValue'] = 'configured' + controller = property(controller_type, default_value(controller_type['ValueType'])) + migrator.send(:clear_inactive_default_captions!, + { 'goCaption' => caption, 'go' => controller }, + 'goCaption' => caption_type, 'go' => controller_type) + expect(caption['Value']['PrimitiveValue']).to eq('configured') + + mappings = { + ['Old', nil] => { key: 'New' }, + %w[OldOption Yes] => { key: 'NewOption', option: 'On' } + } + migrator.instance_variable_set(:@design_alias_mappings, mappings) + items = [ + { 'Key' => 'Old', 'Value' => { 'Option' => nil } }, + { 'Key' => 'OldOption', 'Value' => { 'Option' => 'Yes' } }, + { 'Key' => 'Untouched', 'Value' => { 'Option' => 'No' } } + ] + expect(migrator.send(:migrate_simple_design_aliases!, items)).to eq(2) + expect(items[0]['Key']).to eq('New') + expect(items[1]['Value']['Option']).to eq('On') + + spacing_target = { key: 'Spacing', child: 'margin-top', option: 'M' } + migrator.instance_variable_set(:@spacing_mappings, 'Old::M' => spacing_target) + compound = migrator.send(:compound_design_property, 'Spacing') + compound['Value']['Properties'] = array([ + migrator.send(:option_design_property, 'margin-top', 'M') + ]) + legacy = { + '$Type' => 'Forms$DesignPropertyValue', 'Key' => 'Old', + 'Value' => { '$Type' => 'Forms$OptionDesignPropertyValue', 'Option' => 'M' } + } + owner = { 'DesignProperties' => array([compound, legacy, Marshal.load(Marshal.dump(legacy))]) } + design_counts = { design_properties: 0 } + migrator.send(:migrate_design_properties!, owner, 'u', '$', issues, design_counts) + expect(design_counts[:design_properties]).to eq(2) + + conflicts = {} + first = { 'name' => 'One', 'type' => 'Spacing', + 'margin' => [{ 'name' => 'M', 'top' => { 'oldNames' => ['same'] } }] } + second = { 'name' => 'Two', 'type' => 'Spacing', + 'margin' => [{ 'name' => 'L', 'top' => { 'oldNames' => ['same'] } }] } + migrator.send(:collect_spacing_mappings, first, conflicts) + migrator.send(:collect_spacing_mappings, second, conflicts) + expect(conflicts['same'][:key]).to eq('One') + + Dir.mktmpdir do |dir| + FileUtils.mkdir_p(File.join(dir, 'widgets')) + File.write(File.join(dir, 'widgets', 'broken.mpk'), 'not a zip') + Zip::File.open(File.join(dir, 'widgets', 'other.mpk'), create: true) do |zip| + zip.get_output_stream('Widget.xml') do |stream| + stream.write('Other') + end + end + design = File.join(dir, 'themesource', 'theme', 'web') + FileUtils.mkdir_p(design) + File.write(File.join(design, 'design-properties.json'), '{broken') + fresh = described_class.new(File.join(dir, 'App.mpr')) + expect(fresh.send(:definition, 'absent')).to be_nil + expect(fresh.send(:spacing_mappings)).to eq({}) + expect(fresh.send(:design_property_documents)).to eq([]) + expect(described_class.new('/tmp/x.mpr').send(:package_digest, 'x')).to be_nil + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/frontend_migrator_spec.rb b/spec/frontend_migrator_spec.rb new file mode 100644 index 0000000..6123207 --- /dev/null +++ b/spec/frontend_migrator_spec.rb @@ -0,0 +1,308 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'tmpdir' +require 'zip' + +# rubocop:disable Metrics/BlockLength +RSpec.describe Mxrb::Frontend::Migrator do + def widget_xml(*properties) + body = properties.map do |key, type, default| + default_attribute = default ? %( defaultValue="#{default}") : '' + %(#{key}) + end.join + <<~XML + + Example#{body} + + XML + end + + def package(path, xml) + Zip::File.open(path, create: true) do |zip| + zip.get_output_stream('Widget.xml') { _1.write(xml) } + end + end + + def project(dir, version: '11.12.1') + path = File.join(dir, 'App.mpr') + Mxrb.define(path) do + mendix_version version + self.module(:App) { entity :Item } + end + path + end + + def old_widget(dir, properties) + old = File.join(dir, 'old.mpk') + package(old, widget_xml(*properties)) + type, object = Mxrb::WidgetPackage.template(Mxrb::WidgetPackage.new(old).definition('example.Widget')) + { + '$ID' => SecureRandom.uuid, '$Type' => 'CustomWidgets$CustomWidget', + 'Name' => 'example', 'Type' => type, 'Object' => object + } + end + + def insert_document(path, document) + mpr = Mxrb::IO::MprFile.open(path) + root = mpr.root_unit.fetch('UnitID') + mpr.insert_unit(container_uuid: root, containment_name: 'Documents', contents_doc: document) + ensure + mpr&.close + end + + def array(value) = Mxrb::IO::BsonCodec.parse_array(value)[:items] + + it 'previews and applies an installed widget schema update while preserving values by property key' do + Dir.mktmpdir do |dir| + path = project(dir) + FileUtils.mkdir_p(File.join(dir, 'widgets')) + package(File.join(dir, 'widgets', 'example.mpk'), + widget_xml(%w[mode enumeration cards], %w[debounce integer 300])) + widget = old_widget(dir, [%w[mode enumeration list]]) + array(widget.dig('Object', 'Properties')).first['Value']['TextTemplate'] = { 'stale' => true } + unit_id = insert_document(path, { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Page', 'Name' => 'Home', + 'Widgets' => Mxrb::IO::BsonCodec.build_array([widget]) + }) + + plan = described_class.plan(path) + expect(plan).to be_safe + expect(plan.widgets).to eq(1) + expect(plan.layout_rows).to eq(0) + expect(plan.design_properties).to eq(0) + expect(plan.changes.map(&:unit_id)).to eq([unit_id]) + plan.apply! + expect(plan).to be_applied + + mpr = Mxrb::IO::MprFile.open(path, readonly: true) + migrated = array(mpr.parse_contents(mpr.unit(unit_id))['Widgets']).first + types = array(migrated.dig('Type', 'ObjectType', 'PropertyTypes')) + values = array(migrated.dig('Object', 'Properties')) + by_key = types.to_h do |type| + property = values.find do |candidate| + Mxrb::IO::BsonCodec.extract_id(candidate['TypePointer']) == + Mxrb::IO::BsonCodec.extract_id(type['$ID']) + end + [type['PropertyKey'], property.dig('Value', 'PrimitiveValue')] + end + expect(by_key).to eq('mode' => 'list', 'debounce' => '300') + expect(values.first.dig('Value', 'TextTemplate')).to be_nil + ordered_keys = values.map do |property| + types.find do |type| + Mxrb::IO::BsonCodec.extract_id(property['TypePointer']) == + Mxrb::IO::BsonCodec.extract_id(type['$ID']) + end['PropertyKey'] + end + expect(ordered_keys).to eq(%w[mode debounce]) + mpr.close + + settled = described_class.plan(path) + expect(settled.changes).to be_empty + expect(settled).to be_safe + end + end + + it 'fails closed when an installed schema removes a used property' do + Dir.mktmpdir do |dir| + path = project(dir) + FileUtils.mkdir_p(File.join(dir, 'widgets')) + package(File.join(dir, 'widgets', 'example.mpk'), widget_xml(['replacement', 'string', nil])) + widget = old_widget(dir, [['legacy', 'string', nil]]) + property = array(widget.dig('Object', 'Properties')).first + property['Value']['PrimitiveValue'] = 'important' + insert_document(path, { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Page', 'Name' => 'Home', 'Widget' => widget + }) + + plan = described_class.plan(path) + expect(plan).not_to be_safe + expect(plan.issues.map(&:kind)).to include(:removed_configured_widget_property) + expect { plan.apply! }.to raise_error(Mxrb::SerializationError, /removed_configured_widget_property/) + end + end + + it 'losslessly converts boolean widget values to expressions' do + Dir.mktmpdir do |dir| + path = project(dir) + FileUtils.mkdir_p(File.join(dir, 'widgets')) + package(File.join(dir, 'widgets', 'example.mpk'), widget_xml(%w[required expression false])) + widget = old_widget(dir, [%w[required boolean false]]) + array(widget.dig('Object', 'Properties')).first['Value']['PrimitiveValue'] = 'true' + unit_id = insert_document(path, { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Page', 'Name' => 'Home', 'Widget' => widget + }) + + plan = described_class.plan(path) + expect(plan).to be_safe + plan.apply! + mpr = Mxrb::IO::MprFile.open(path, readonly: true) + value = array(mpr.parse_contents(mpr.unit(unit_id)).dig('Widget', 'Object', 'Properties')).first['Value'] + expect(value).to include('Expression' => 'true', 'PrimitiveValue' => '') + mpr.close + end + end + + it 'reconciles a current widget schema whose object still needs model normalization' do + Dir.mktmpdir do |dir| + path = project(dir) + FileUtils.mkdir_p(File.join(dir, 'widgets')) + properties = [['source', 'datasource', nil]] + package(File.join(dir, 'widgets', 'example.mpk'), widget_xml(*properties)) + widget = old_widget(dir, properties) + value = array(widget.dig('Object', 'Properties')).first.fetch('Value') + value['DataSource'] = { + '$Type' => 'Forms$MicroflowSettings', 'Microflow' => 'Demo.Source' + } + unit_id = insert_document(path, { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Page', 'Name' => 'Home', 'Widget' => widget + }) + + plan = described_class.plan(path) + expect(plan).to be_safe + expect(plan.widgets).to eq(1) + plan.apply! + + mpr = Mxrb::IO::MprFile.open(path, readonly: true) + migrated = mpr.parse_contents(mpr.unit(unit_id)).dig('Widget', 'Object', 'Properties') + data_source = array(migrated).first.dig('Value', 'DataSource') + expect(array(data_source['OutputMappings'])).to be_empty + expect(Mxrb::IO::BsonCodec.parse_array(data_source['OutputMappings'])[:marker]).to eq(3) + mpr.close + expect(described_class.plan(path).changes).to be_empty + end + end + + it 'blocks missing, ambiguous, and invalid installed widget packages' do + Dir.mktmpdir do |dir| + path = project(dir) + widget = old_widget(dir, [%w[mode enumeration list]]) + insert_document(path, { '$Type' => 'Forms$Page', 'Widget' => widget }) + plan = described_class.plan(path) + expect(plan).not_to be_safe + expect(plan.issues.map(&:kind)).to include(:missing_widget_definition) + end + + Dir.mktmpdir do |dir| + path = project(dir) + widgets = File.join(dir, 'widgets') + FileUtils.mkdir_p(widgets) + xml = widget_xml(%w[mode enumeration list]) + package(File.join(widgets, 'a.mpk'), xml) + package(File.join(widgets, 'b.mpk'), xml) + insert_document(path, { '$Type' => 'Forms$Page', 'Widget' => old_widget(dir, [%w[mode enumeration list]]) }) + plan = described_class.plan(path) + expect(plan).not_to be_safe + expect(plan.issues.map(&:kind)).to include(:ambiguous_widget_definition) + end + + Dir.mktmpdir do |dir| + path = project(dir) + widgets = File.join(dir, 'widgets') + FileUtils.mkdir_p(widgets) + File.binwrite(File.join(widgets, 'broken.mpk'), 'not a zip') + plan = described_class.plan(path) + expect(plan).not_to be_safe + expect(plan.issues.map(&:kind)).to include(:invalid_widget_package) + end + end + + it 'normalizes legacy proportional layout weights without changing their ordering' do + Dir.mktmpdir do |dir| + path = project(dir, version: '10.24.0.73019') + rows = [ + [-1], [-2, -1, -2], [3, -1, -2] + ].map do |weights| + { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$LayoutGridRow', + 'Columns' => Mxrb::IO::BsonCodec.build_array(weights.map do |weight| + { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$LayoutGridColumn', + 'Weight' => weight, 'TabletWeight' => -1, 'PhoneWeight' => -1 + } + end) + } + end + unit_id = insert_document(path, { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Page', 'Name' => 'Grid', 'Rows' => rows + }) + + plan = described_class.plan(path) + expect(plan).to be_safe + expect(plan.layout_rows).to eq(3) + plan.apply! + mpr = Mxrb::IO::MprFile.open(path, readonly: true) + migrated = mpr.parse_contents(mpr.unit(unit_id))['Rows'] + expect(migrated.map { |row| array(row['Columns']).map { _1['Weight'] } }) + .to eq([[12], [5, 2, 5], [3, 3, 6]]) + expect(migrated.flat_map { |row| array(row['Columns']) }.map { _1['TabletWeight'] }.uniq) + .to eq([-1]) + mpr.close + end + end + + it 'blocks zero, overcommitted and unsupported layout contracts' do + Dir.mktmpdir do |dir| + path = project(dir) + rows = [[0, -1], [13, -1]].map do |weights| + { + '$Type' => 'Forms$LayoutGridRow', + 'Columns' => weights.map do |weight| + { '$Type' => 'Forms$LayoutGridColumn', 'Weight' => weight, + 'TabletWeight' => weight, 'PhoneWeight' => weight } + end + } + end + insert_document(path, { '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Page', 'Rows' => rows }) + plan = described_class.plan(path) + expect(plan).not_to be_safe + expect(plan.issues.map(&:kind).uniq).to eq([:unsafe_layout_weights]) + end + + Dir.mktmpdir do |dir| + plan = described_class.plan(project(dir, version: '9.24.0')) + expect(plan).not_to be_safe + expect(plan.issues.map(&:kind)).to eq([:unsupported_version]) + end + end + + it 'losslessly folds legacy spacing aliases into compound theme properties' do + Dir.mktmpdir do |dir| + path = project(dir) + theme = File.join(dir, 'themesource', 'atlas_core', 'web') + FileUtils.mkdir_p(theme) + File.write(File.join(theme, 'design-properties.json'), JSON.pretty_generate( + 'Widget' => [{ + 'name' => 'Spacing', 'type' => 'Spacing', + 'margin' => [{ 'name' => 'M', 'bottom' => { + 'oldNames' => ['Spacing bottom::Outer medium'] + } }] + }] + )) + appearance = { + 'DesignProperties' => Mxrb::IO::BsonCodec.build_array([{ + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$DesignPropertyValue', + 'Key' => 'Spacing bottom', 'Value' => { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$OptionDesignPropertyValue', + 'Option' => 'Outer medium' + } + }]) + } + unit_id = insert_document(path, { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$Page', 'Appearance' => appearance + }) + + plan = described_class.plan(path) + expect(plan).to be_safe + expect(plan.design_properties).to eq(1) + plan.apply! + mpr = Mxrb::IO::MprFile.open(path, readonly: true) + properties = array(mpr.parse_contents(mpr.unit(unit_id)).dig('Appearance', 'DesignProperties')) + expect(properties.first['Key']).to eq('Spacing') + nested = array(properties.first.dig('Value', 'Properties')).first + expect(nested).to include('Key' => 'margin-bottom', 'Value' => include('Option' => 'M')) + mpr.close + end + end +end +# rubocop:enable Metrics/BlockLength diff --git a/spec/functional_spec.rb b/spec/functional_spec.rb index e57c9e7..1812452 100644 --- a/spec/functional_spec.rb +++ b/spec/functional_spec.rb @@ -276,6 +276,18 @@ def build_functional_mpr(path, java: "21") expect(plan).not_to be_available end + it "accepts runtime distributions without PAD templates" do + plan = Mxrb::Runtime::Plan.new( + "10.24.0.73019", "21", File.join(@toolchains, "10.24.0.73019"), + "mx", "mxbuild", "jdk", "jre", "builder", "runtime" + ) + launcher = File.join(plan.runtime_path, "launcher", "runtimelauncher.jar") + FileUtils.mkdir_p(File.dirname(launcher)) + File.write(launcher, "runtime") + + expect(plan).to be_available + end + it "reads the Java family from project settings through the public API" do mpr = Mxrb::IO::MprFile.open(@path) mpr.insert_unit( diff --git a/spec/generic_widget_bundle_compiler_spec.rb b/spec/generic_widget_bundle_compiler_spec.rb index 1d70e1a..97eb584 100644 --- a/spec/generic_widget_bundle_compiler_spec.rb +++ b/spec/generic_widget_bundle_compiler_spec.rb @@ -92,5 +92,167 @@ def property(id, value) expect(described_class.new(source, 'Demo.Home', widget, scope: nil, entity: nil)) .not_to be_supported end + + it 'covers every schema-property, expression, template, and package fallback' do # rubocop:disable Metrics/BlockLength + Dir.mktmpdir do |root| + index = {} + source = instance_double( + Mxrb::Compiler::SourceModel, + path: File.join(root, 'App.mpr'), document_index: index + ) + allow(source).to receive(:units_of).and_return([]) + compiler = described_class.allocate + compiler.instance_variable_set(:@source, source) + compiler.instance_variable_set(:@page_name, 'Demo.Home') + compiler.instance_variable_set(:@widget, { 'Name' => 'all', 'Appearance' => {} }) + compiler.instance_variable_set(:@scope, 'p.Demo.Home.context') + compiler.instance_variable_set(:@entity, 'Demo.Item') + compiler.instance_variable_set(:@render_widgets, ->(widgets) { "rendered-#{widgets.length}" }) + compiler.instance_variable_set(:@key_prefix, 'p') + compiler.instance_variable_set(:@action_property, ->(_action) { 'ActionProperty({})' }) + compiler.instance_variable_set(:@index, index) + compiler.instance_variable_set(:@values, {}) + compiler.instance_variable_set(:@list_data_source, nil) + compiler.instance_variable_set(:@component_name, 'All') + compiler.instance_variable_set(:@module_path, 'example/All') + + expect(compiler.send(:widget_id)).to be_nil + expect(compiler.send(:property_values, nil)).to eq({}) + expect(compiler.send(:property_values, 'Properties' => [2, { 'TypePointer' => 'missing' }])).to eq({}) + + primitive_types = %w[Boolean Integer Decimal Enumeration String System] + primitive_types.each { expect(compiler.send(:supported_property?, [_1, {}])).to be(true) } + expect(compiler.send(:supported_property?, ['Association', {}])).to be(true) + expect(compiler.send(:supported_property?, ['Attribute', { 'AttributeRef' => nil }])).to be(true) + expect(compiler.send(:supported_property?, ['DataSource', { 'DataSource' => nil }])).to be(true) + expect(compiler.send(:supported_property?, ['DataSource', { 'DataSource' => { '$Type' => 'Bad' } }])) + .to be(false) + expect(compiler.send(:supported_property?, ['Object', { 'Objects' => [2] }])).to be(true) + expect(compiler.send(:supported_property?, ['Object', { 'Objects' => [2, {}] }])).to be(false) + expect(compiler.send(:supported_property?, ['Unknown', {}])).to be(false) + + values = { + 'Boolean' => { 'PrimitiveValue' => 'false' }, 'Integer' => { 'PrimitiveValue' => '12' }, + 'Decimal' => { 'PrimitiveValue' => '1.5' }, 'Enumeration' => { 'PrimitiveValue' => 'One' }, + 'String' => { 'PrimitiveValue' => 'value' }, 'Association' => {}, 'Object' => {} + } + values.each { |type, value| expect { compiler.send(:compile_property, [type, value]) }.not_to raise_error } + expect(compiler.send(:compile_property, ['Unknown', {}])).to eq(:undefined) + expect(compiler.send(:compile_property, ['DataSource', { 'DataSource' => { '$Type' => 'Bad' } }])) + .to eq(:undefined) + expect(compiler.send(:compile_property, ['Action', { + 'Action' => { '$Type' => 'Forms$NoAction' } + }])).to eq(:undefined) + expect(compiler.send(:compile_widgets, 'Widgets' => [2])).to eq([]) + expect(compiler.send(:compile_widgets, 'Widgets' => [2, {}])).to eq('rendered-1') + + expressions = ['', 'empty', '$Item', '$Item/Name', '12', '-1.5', 'true', 'false', "'text'", 'invalid +'] + compiled = expressions.map { compiler.send(:compile_expression, _1) } + expect(compiled).to include(nil, { type: 'variable', variable: 'currentObject' }, + { type: 'literalNumeric', value: '-1.5' }) + expect(compiler.send(:variable_expression, '$Item/Name')).to include(path: 'Name') + + literal = { 'TextTemplate' => { 'Template' => { 'Items' => [2] }, 'Parameters' => [2] } } + expect(compiler.send(:compile_template, literal)).to eq(type: 'literal', value: '') + invalid = { 'TextTemplate' => { 'Template' => { 'Items' => [2] }, + 'Parameters' => [2, { 'Expression' => 'bad +' }] } } + expect(compiler.send(:compile_template, invalid)).to be_nil + expect(compiler.send(:compile_template, 'TextTemplate' => nil)).to eq(type: 'literal', value: '') + single = { 'TextTemplate' => { 'Template' => { 'Items' => [2, { 'Text' => '{1}' }] }, + 'Parameters' => [2, { 'Expression' => '$Item' }] } } + expect(compiler.send(:compile_template, single)).to include(type: 'variable') + formatted = single.dup + formatted['TextTemplate'] = Marshal.load(Marshal.dump(single['TextTemplate'])) + formatted.dig('TextTemplate', 'Template', 'Items', 1)['Text'] = 'Item {1}' + expect(compiler.send(:compile_template, formatted)).to include(type: 'function', name: '+') + + expect(compiler.send(:attribute_property, 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name' })) + .to include('AttributeProperty') + expect(compiler.send(:supported_property?, ['Attribute', { + 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name' } + }])).to be(true) + expect(compiler.send(:compile_property, ['Attribute', { + 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name' } + }])).to include('$raw' => include('AttributeProperty')) + compiler.instance_variable_set(:@scope, nil) + expect(compiler.send(:compile_property, ['Attribute', { + 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name' } + }])).to eq(:undefined) + expect(compiler.send(:attribute_property, 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name' })).to be_nil + expect(compiler.send(:attribute_property, 'AttributeRef' => { 'Attribute' => 'Name' })).to be_nil + compiler.instance_variable_set(:@scope, 'scope') + + nested_expression = { type: 'function', parameters: [{ type: 'variable', variable: 'currentObject' }] } + expect(compiler.send(:expression_variables, nested_expression)).to eq(['currentObject']) + expect(compiler.send(:expression_variables, [nested_expression, nil])).to eq(['currentObject']) + expect(compiler.send(:expression_variables, 'literal')).to eq([]) + + xpath = { 'DataSource' => { '$Type' => 'CustomWidgets$CustomWidgetXPathSource', + 'EntityRef' => { 'Entity' => 'Demo.Item' }, 'SortBar' => { + 'SortItems' => [2, { 'AttributeRef' => { 'Attribute' => '' } }, { + 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name' }, + 'SortOrder' => 'Descending' + }] + } } } + expect(compiler.send(:database_list_property, xpath)).to include('"desc"') + expect(compiler.send(:database_list_property, 'DataSource' => { + '$Type' => 'CustomWidgets$CustomWidgetXPathSource', 'EntityRef' => { 'Entity' => 'Bad' } + })).to be_nil + compiler.instance_variable_set(:@list_data_source, xpath) + expect(compiler.send(:expression_property, 'Expression', 'Expression' => '$Item')) + .to include('ListExpressionProperty', 'dataSourceId') + expect(compiler.send(:expression_property, 'Expression', 'Expression' => 'bad +')).to be_nil + compiler.instance_variable_set(:@list_data_source, nil) + expect(compiler.send(:expression_property, 'Expression', 'Expression' => '$Item')) + .to include('ExpressionProperty') + + expect(compiler.send(:compiled_action, 'Action' => { '$Type' => 'Forms$NoAction' })).to be_nil + expect(compiler.send(:compiled_action, 'Action' => { '$Type' => 'Forms$MicroflowAction' })) + .to include('ActionProperty') + compiler.instance_variable_set(:@action_property, nil) + expect(compiler.send(:compiled_action, 'Action' => { '$Type' => 'Forms$MicroflowAction' })).to be_nil + expect(compiler.send(:no_action?, {})).to be(true) + expect(compiler.send(:no_action?, 'Action' => { '$Type' => 'Forms$NoAction' })).to be(true) + expect(compiler.send(:no_action?, 'Action' => { '$Type' => 'Other' })).to be(false) + + expect(compiler.send(:enumeration_name, 'Missing.Entity.Member')).to be_nil + enumeration_domain = Struct.new(:module_name, :document).new('Demo', { + 'Entities' => [2, { 'Name' => 'Item', 'Attributes' => [2, { + 'Name' => 'State', 'NewType' => { 'Enumeration' => 'Demo.State' } + }, { 'Name' => 'Name', 'NewType' => {} }] }] + }) + allow(source).to receive(:units_of).with('DomainModels$DomainModel').and_return([enumeration_domain]) + expect(compiler.send(:enumeration_name, 'Demo.Item.State')).to eq('Demo.State') + expect(compiler.send(:enumeration_name, 'Demo.Item.Name')).to be_nil + expect(compiler.send(:template_parameter, 'AttributeRef' => { + 'Attribute' => 'Demo.Item.State' + })).to include(type: 'function', name: 'getCaption') + expect(compiler.send(:template_parameter, 'AttributeRef' => { + 'Attribute' => 'Demo.Item.Name' + })).to include(type: 'variable') + expect(compiler.send(:format_expression, '{1} suffix', [{ type: 'literal', value: 'x' }])) + .to include(type: 'function') + expect(compiler.send(:translated_text, nil)).to eq('') + expect(compiler.send(:present_qualified_name?, 'Demo.Item')).to be(true) + expect(compiler.send(:present_qualified_name?, 'Bad')).to be(false) + expect(compiler.send(:quoted?, '"yes"')).to be(true) + expect(compiler.send(:quoted?, "'no\"")).to be(false) + + FileUtils.mkdir_p(File.join(root, 'widgets', 'example')) + File.write(File.join(root, 'widgets', 'example', 'All.mjs'), '') + expect(compiler.send(:package_module?)).to be(true) + + compiler.instance_variable_set(:@values, 'association' => ['Association', {}]) + expect(compiler.render).not_to include('"association"') + FileUtils.rm_f(File.join(root, 'widgets', 'example', 'All.mjs')) + FileUtils.mkdir_p(File.join(root, 'widgets')) + File.write(File.join(root, 'widgets', 'broken.mpk'), 'broken') + expect(compiler.send(:package_module?)).to be(false) + Zip::File.open(File.join(root, 'widgets', 'good.mpk'), create: true) do |zip| + zip.get_output_stream('example/All.mjs') { _1.write('') } + end + expect(compiler.send(:package_module?)).to be(true) + end + end end # rubocop:enable Metrics/BlockLength diff --git a/spec/legacy_page_builder_spec.rb b/spec/legacy_page_builder_spec.rb index 2a0f38d..3592bbb 100644 --- a/spec/legacy_page_builder_spec.rb +++ b/spec/legacy_page_builder_spec.rb @@ -123,7 +123,9 @@ def grid(source_type) # rubocop:disable Metrics/MethodLength 'Widgets' => [3] }] } } ) - source = instance_double(Mxrb::Compiler::SourceModel, units_of: [page], documents: [page.document]) + source = instance_double(Mxrb::Compiler::SourceModel, documents: [page.document]) + allow(source).to receive(:units_of).with('Forms$Page').and_return([page]) + allow(source).to receive(:units_of).with('Forms$Layout').and_return([]) result = described_class.new(source, root, profiles: %i[dojo react_wrapper]).build expect(result.files).to eq(2) expect(result.unsupported_widgets).to be_empty @@ -153,7 +155,9 @@ def grid(source_type) # rubocop:disable Metrics/MethodLength 'Widgets' => [3, grid, { '$Type' => 'Forms$TextBox', 'Name' => 'name' }] } ) - source = instance_double(Mxrb::Compiler::SourceModel, units_of: [page], documents: [page.document]) + source = instance_double(Mxrb::Compiler::SourceModel, documents: [page.document]) + allow(source).to receive(:units_of).with('Forms$Page').and_return([page]) + allow(source).to receive(:units_of).with('Forms$Layout').and_return([]) result = described_class.new(source, root).build xml = File.binread("#{result.directory}/pt_BR/Demo/Home.page.xml").force_encoding('UTF-8') @@ -165,16 +169,124 @@ def grid(source_type) # rubocop:disable Metrics/MethodLength end it 'audits pages without writing files and includes custom widgets' do + grid = { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$DataGrid', 'Name' => 'items', + 'Columns' => [2], 'DataSource' => { + '$Type' => 'Forms$GridXPathSource', 'EntityPath' => 'Demo.Item' + }, 'ControlBar' => {}, 'IsPagingEnabled' => false + } + dynamic = { + '$Type' => 'Forms$DynamicText', 'Content' => { + 'Parameters' => [2, { 'Expression' => '$Item/Name' }] + } + } page = Mxrb::Compiler::SourceModel::Unit.new( id: SecureRandom.uuid, container_id: nil, containment: nil, module_name: 'Demo', document: { '$Type' => 'Forms$Page', 'Name' => 'Home', 'Widgets' => [3, { '$Type' => 'Forms$TextBox' }, - { '$Type' => 'CustomWidgets$CustomWidget' }] } + { '$Type' => 'CustomWidgets$CustomWidget' }, grid, dynamic] } + ) + source = instance_double(Mxrb::Compiler::SourceModel, documents: [page.document]) + allow(source).to receive(:units_of).with('Forms$Page').and_return([page]) + allow(source).to receive(:units_of).with('Forms$Layout').and_return([]) + + expect(described_class.new(source, '/not-used').audit).to eq( + 'Demo.Home' => ['CustomWidgets$CustomWidget', 'Forms$DynamicText(parameters)', 'Forms$TextBox'] ) - source = instance_double(Mxrb::Compiler::SourceModel, units_of: [page], documents: [page.document]) + end - expect(described_class.new(source, '/not-used').audit) - .to eq('Demo.Home' => %w[CustomWidgets$CustomWidget Forms$TextBox]) + it 'renders layouts, placeholders, containers and literal dynamic text natively' do + Dir.mktmpdir do |root| + placeholder_id = SecureRandom.uuid + layout = Mxrb::Compiler::SourceModel::Unit.new( + id: SecureRandom.uuid, container_id: nil, containment: nil, module_name: 'Demo', + document: { + '$Type' => 'Forms$Layout', 'Name' => 'Shell', + 'Widget' => { '$ID' => placeholder_id, '$Type' => 'Forms$Placeholder', 'Name' => 'Main' } + } + ) + text = { + '$Type' => 'Forms$DynamicText', 'Name' => 'heading', 'RenderMode' => 'Text', + 'Content' => { + 'Parameters' => [2], + 'Template' => { 'Items' => [2, { 'LanguageCode' => 'en_US', 'Text' => 'Hello & welcome' }] } + } + } + page = Mxrb::Compiler::SourceModel::Unit.new( + id: SecureRandom.uuid, container_id: nil, containment: nil, module_name: 'Demo', + document: { + '$Type' => 'Forms$Page', 'Name' => 'Home', 'Class' => 'app-page', + 'Title' => { 'Items' => [2, { 'LanguageCode' => 'en_US', 'Text' => 'Home' }] }, + 'FormCall' => { + 'Form' => 'Demo.Shell', + 'Arguments' => [2, { 'Parameter' => 'Demo.Shell.Main', + 'Widget' => { + '$Type' => 'Forms$VerticalFlow', 'Widgets' => [2, { + '$Type' => 'Forms$DivContainer', 'Name' => 'card', + 'Class' => 'panel', 'Widget' => text + }] + } }] + } + } + ) + source = instance_double(Mxrb::Compiler::SourceModel, documents: [layout.document, page.document]) + allow(source).to receive(:units_of).with('Forms$Page').and_return([page]) + allow(source).to receive(:units_of).with('Forms$Layout').and_return([layout]) + + result = described_class.new(source, root).build + page_xml = File.binread(File.join(root, 'pages/en_US/Demo/Home.page.xml')).force_encoding('UTF-8') + layout_xml = File.binread(File.join(root, 'pages/en_US/Demo/Shell.layout.xml')).force_encoding('UTF-8') + + expect(result).to have_attributes(files: 2, unsupported_widgets: {}) + expect(page_xml).to include( + "parameterName='#{placeholder_id}'", "class='mx-name-card panel'", + "class='mx-text mx-name-heading'", 'Hello & welcome' + ) + expect(layout_xml).to include("data-mx-placeholder='#{placeholder_id}'") + expect { REXML::Document.new(page_xml) }.not_to raise_error + expect { REXML::Document.new(layout_xml) }.not_to raise_error + end + end + + it 'covers defensive legacy rendering fallbacks and interactive-container auditing' do + layout = Mxrb::Compiler::SourceModel::Unit.new( + id: 'layout', container_id: nil, containment: nil, module_name: 'Demo', + document: { '$Type' => 'Forms$Layout', 'Name' => 'Shell', 'Widgets' => [2] } + ) + source = instance_double(Mxrb::Compiler::SourceModel, documents: []) + allow(source).to receive(:units_of).with('Forms$Layout').and_return([layout]) + allow(source).to receive(:units_of).with('Forms$Page').and_return([]) + builder = described_class.new(source, '/not-used') + builder.instance_variable_set(:@widget_sequence, 0) + + interactive = { + '$Type' => 'Forms$DivContainer', + 'OnClickAction' => { '$Type' => 'Forms$MicroflowAction' } + } + builder.send(:audit_unsupported_widgets, interactive, 'Demo.Home') + expect(builder.instance_variable_get(:@unsupported)['Demo.Home']) + .to include('Forms$DivContainer(onClick)') + expect(builder.send(:render_widget, nil, 'Demo.Home', 'en_US')).to eq('') + + span = { '$Type' => 'Forms$DivContainer', 'RenderMode' => 'Span', 'Style' => 'color:red' } + expect(builder.send(:render_widget, span, 'Demo.Home', 'en_US')) + .to include(' 'Forms$DynamicText', 'RenderMode' => 'Paragraph', + 'Content' => { + 'Template' => { 'Items' => [2] }, + 'Fallback' => { 'Items' => [2, { 'LanguageCode' => 'en_US', 'Text' => 'Fallback' }] } + } + } + expect(builder.send(:render_widget, paragraph, 'Demo.Home', 'en_US')) + .to include(' { 'Style' => 'display:block' } }, base: nil)) + .to eq(" style='display:block'") + expect(builder.send(:css_classes, { 'Name' => '' }, base: nil)).to eq('') + expect(builder.send(:widget_children, nil)).to eq([]) + expect(builder.send(:layout_argument_id, 'Demo.Missing.Main')).to eq('Demo.Missing.Main') + expect(builder.send(:layout_argument_id, 'Demo.Shell.Missing')).to eq('Demo.Shell.Missing') end end # rubocop:enable Metrics/BlockLength diff --git a/spec/nanoflow_program_compiler_spec.rb b/spec/nanoflow_program_compiler_spec.rb index f2db882..b3d5eee 100644 --- a/spec/nanoflow_program_compiler_spec.rb +++ b/spec/nanoflow_program_compiler_spec.rb @@ -119,5 +119,94 @@ def build_source # rubocop:disable Metrics/MethodLength expect(compiler.unsupported).to be_empty end end + + it 'covers defensive graph, action, parameter, and expression decisions' do # rubocop:disable Metrics/BlockLength + build_source do |source| + compiler = described_class.new(source) + id = ->(suffix) { { '$ID' => "11111111-1111-4111-8111-#{suffix.to_s.rjust(12, '0')}" } } + node = id.call(1).merge('$Type' => 'Microflows$ActionActivity') + + expect(compiler.send(:reachable_nodes, nil, {}, {})).to eq([]) + expect(compiler.send(:reachable_nodes, id.call(2), {}, id.call(2)['$ID'] => [{ + 'DestinationPointer' => id.call(99) + }])).to eq([]) + expect(compiler.send(:compile_graph_node, node.merge('Action' => { + '$Type' => 'Microflows$CreateVariableAction', 'VariableName' => 'x' + }), [])).to contain_exactly(include(type: 'setVariable')) + expect(compiler.send(:compile_graph_node, node.merge('$Type' => 'Microflows$EndEvent'), [])) + .to contain_exactly(include(type: 'return')) + expect(compiler.send(:compile_try_catch, node, [], [])).to be_nil + normal = { 'DestinationPointer' => id.call(8) } + error = { 'IsErrorHandler' => true, 'DestinationPointer' => id.call(9) } + expect(compiler.send(:compile_try_catch, node.merge('Action' => { + '$Type' => 'Microflows$CreateObjectAction', 'Entity' => 'Demo.Item', 'VariableName' => 'Item' + }), [normal], [error])).to include(type: 'tryCatch', body: be_an(Array)) + expect(compiler.send(:compile_try_catch, node.merge('Action' => { + '$Type' => 'Microflows$Unsupported' + }), [normal], [error])).to be_nil + + create = { + '$Type' => 'Microflows$CreateObjectAction', 'Entity' => 'Demo.Item', + 'VariableName' => 'Item', 'Commit' => 'Yes', 'Items' => [2, { + '$Type' => 'Microflows$MemberChange', 'Attribute' => 'Demo.Item.Name', + 'Type' => 'Set', 'Value' => "'Name'" + }] + } + expect(compiler.send(:compile_create_object, create, node)).to include(include(type: 'commitObjects')) + expect(compiler.send(:compile_node, node.merge('Action' => { + '$Type' => 'Microflows$CommitAction', 'CommitVariableName' => 'Item' + }))).to include(type: 'commitObjects') + expect(compiler.send(:change_instructions, { 'Items' => [2, { + '$Type' => 'Bad', 'Attribute' => '', 'Type' => 'Add' + }] }, 'Item', 'change')).to be_empty + expect(compiler.send(:compile_microflow_call, { '$Type' => 'Call', 'MicroflowCall' => {} }, node)).to be_nil + expect(compiler.send(:compile_javascript_call, { + '$Type' => 'JavaScript', 'JavaScriptAction' => 'Demo.Missing' + }, node)).to be_nil + + expect(compiler.send(:quoted_entity, 'Demo.Item')).to eq('"Demo.Item"') + expect(compiler.send(:quoted_entity, '')).to be_nil + expect(compiler.send(:javascript_parameter_kind, { + 'ParameterType' => { 'Type' => { '$Type' => 'DataTypes$EntityType' } } + })).to eq('object') + expect(compiler.send(:javascript_parameter_kind, { + 'ParameterType' => { 'Type' => { '$Type' => 'DataTypes$ListType' } } + })).to eq('list') + + no_case = { '$Type' => 'Microflows$NoCase' } + expect(compiler.send(:compile_split, id.call(3).merge( + 'SplitCondition' => { 'Expression' => '$ready' } + ), [{ 'CaseValues' => [2, no_case], 'DestinationPointer' => id.call(4) }])) + .to include(targets: { '' => id.call(4)['$ID'] }) + expect(compiler.send(:compile_split, id.call(3), [])).to be_nil + expect(compiler.send(:compile_split, id.call(3).merge( + 'SplitCondition' => { 'Expression' => '$ready' } + ), [{ 'CaseValues' => [2], 'DestinationPointer' => id.call(4) }])) + .to include(targets: include('')) + expect(compiler.send(:compile_merge, id.call(3), [])).to be_nil + expect(compiler.send(:compile_show_form, { '$Type' => 'Show', 'FormSettings' => {} }, node)).to be_nil + expect(compiler.send(:compile_validation, { '$Type' => 'Validation', 'Attribute' => '' }, node)).to be_nil + expect(compiler.send(:compile_commit, { '$Type' => 'Commit' }, node)).to be_nil + expect(compiler.send(:compile_commit, { 'CommitVariableName' => 'Item' }, node, label: false)) + .not_to have_key(:label) + + expect(compiler.send(:text_template_expression, nil)).to eq(type: 'literal', value: nil) + localized = { 'Text' => { 'Items' => [2, { 'LanguageCode' => 'pt_BR', 'Text' => 'Oi' }] }, + 'Parameters' => [2, { 'Expression' => '$Name' }] } + expect(compiler.send(:text_template_expression, localized)).to include(type: 'function') + expect(compiler.send(:variable_kind, '$Type' => 'DataTypes$ObjectType')).to eq('object') + expect(compiler.send(:variable_kind, nil)).to eq('primitive') + + ['not($ready)', 'isNew($Item)', '$a and $b', '$a = $b', 'if true then 1 else 2'].each do |expression| + expect(compiler.send(:expression, expression)).to be_a(Hash) + end + expect(compiler.send(:current_variable_kinds)).to eq({}) + compiler.instance_variable_set(:@variable_kind_stack, [{ 'Item' => 'object' }]) + expect(compiler.send(:expression_kind, '$Item')).to eq('object') + expect(compiler.send(:function_expression, 'unknown()')).to be_nil + expect(compiler.send(:binary_expression, 'plain')).to be_nil + expect(compiler.send(:conditional_expression, 'plain')).to be_nil + end + end end # rubocop:enable Metrics/BlockLength diff --git a/spec/native_bootstrap_spec.rb b/spec/native_bootstrap_spec.rb index 2931e1a..946be21 100644 --- a/spec/native_bootstrap_spec.rb +++ b/spec/native_bootstrap_spec.rb @@ -142,6 +142,7 @@ def prepare_assets expect(described_class.for('7.17.0-rc5').seed_version).to eq('7.17.0') expect(described_class.for('7.99.0').seed_version).to eq('7.17.0') expect(described_class.for('9.24.0').seed_version).to eq('9.6.1.29396') + expect(described_class.for('10.24.0').seed_version).to eq('10.24.0.73019') expect(described_class.for('11.99.0').seed_version).to eq('11.12.1') end @@ -213,5 +214,14 @@ def prepare_assets expect(Mxrb::Compiler::ModelPackage.read(path).types['Queues$Queue']).to eq(2) end end + + it 'uses expression-based queue configuration in Mendix 10' do + materializer = described_class.allocate + materializer.instance_variable_set(:@version, '10.24.0.73019') + + expect(materializer.send(:queue_config, 'id', '5')).to include( + 'ParallelismExpression' => '5', 'ClusterWide' => false + ) + end end # rubocop:enable Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/official_marketplace_dependency_spec.rb b/spec/official_marketplace_dependency_spec.rb new file mode 100644 index 0000000..6e327d6 --- /dev/null +++ b/spec/official_marketplace_dependency_spec.rb @@ -0,0 +1,266 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'tmpdir' +require 'zip' + +# rubocop:disable Metrics/BlockLength, Metrics/MethodLength +RSpec.describe Mxrb::OfficialMarketplace::DependencyResolver do + def create_project(path, module_name:, dependency: nil) + Mxrb.define(path) do + mendix_version '11.12.1' + self.module module_name do + microflow(:Work) { call_microflow "#{dependency}.Work" if dependency } + end + end + end + + def create_package(path, source, module_name:, version: '1.0.0') + xml = <<~XML + + + + + + + + + XML + manifest = { + 'package' => { 'name' => module_name, 'version' => version, 'type' => 'Module' }, + 'model-version' => '11.12.1' + } + Zip::File.open(path, create: true) do |zip| + zip.add('project.mpr', source) + zip.get_output_stream('package.xml') { _1.write(xml) } + zip.get_output_stream('manifest.json') { _1.write(JSON.generate(manifest)) } + end + path + end + + def package(name, id) + Mxrb::OfficialMarketplace::OfficialPackage.new( + name, '1.0.0', :mendix, "https://marketplace.test/#{id}", "content:#{id}", id, + "00000000-0000-0000-0000-#{id.to_s.rjust(12, '0')}", + 'Module', 'Regular', [], false, true + ) + end + + around do |example| + Dir.mktmpdir do |dir| + @root = File.join(dir, 'project') + FileUtils.mkdir_p(@root) + @target = File.join(@root, 'App.mpr') + host = File.join(dir, 'host.mpr') + root_source = File.join(dir, 'root.mpr') + dependency_source = File.join(dir, 'dependency.mpr') + transitive_source = File.join(dir, 'transitive.mpr') + create_project(host, module_name: :Host) + create_project(root_source, module_name: :RootModule, dependency: 'DependencyModule') + create_project( + dependency_source, module_name: :DependencyModule, dependency: 'TransitiveModule' + ) + create_project(transitive_source, module_name: :TransitiveModule) + FileUtils.cp(host, @target) + @archives = { + 1 => create_package(File.join(dir, 'root.mpk'), root_source, module_name: 'RootModule'), + 2 => create_package( + File.join(dir, 'dependency.mpk'), dependency_source, module_name: 'DependencyModule' + ), + 3 => create_package( + File.join(dir, 'transitive.mpk'), transitive_source, module_name: 'TransitiveModule' + ) + } + @packages = { + 1 => package('Root Module', 1), + 2 => package('Dependency Module', 2), + 3 => package('Transitive Module', 3) + } + @installer = Mxrb::OfficialMarketplace::Installer.new(target: @root, mpr: @target) + @installer.send(:import_module, @archives.fetch(1), @packages.fetch(1), @target) + example.run + end + end + + before do + @api = instance_double(Mxrb::OfficialMarketplace::ContentApi) + allow(@api).to receive(:search) do |name:, **| + id = 2 if name.delete(' _').casecmp('DependencyModule').zero? + id = 3 if name.delete(' _').casecmp('TransitiveModule').zero? + id ? [{ 'contentId' => id, 'type' => 'Module' }] : [] + end + allow(@api).to receive(:resolve) { |id, **| @packages.fetch(id) } + allow(@api).to receive(:download) do |_version_id, destination, download_url:| + id = download_url.split('/').last.to_i + FileUtils.cp(@archives.fetch(id), destination) + destination + end + end + + it 'previews and applies verified dependencies in transitive-first order' do + plan = @installer.dependencies('RootModule', api: @api) + expect(plan).to have_attributes(root: 'RootModule', safe?: true, applied?: false) + expect(plan.changes).to eq( + ['install TransitiveModule 1.0.0', 'install DependencyModule 1.0.0'] + ) + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages').keys).to eq(['RootModule']) + expect { plan.apply! }.to raise_error(Mxrb::MarketplaceError, /preview expired/) + + applied = @installer.dependencies('1', api: @api, apply: true) + expect(applied).to have_attributes(safe?: true, applied?: true) + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages').keys) + .to contain_exactly('RootModule', 'DependencyModule', 'TransitiveModule') + expect(Mxrb.validate(@target)).to be_valid + expect { applied.apply! }.to raise_error(Mxrb::MarketplaceError, /already applied/) + expect(applied.expire!).to equal(applied) + + complete = @installer.dependencies('RootModule', api: @api) + expect(complete).to be_safe + expect(complete.changes).to be_empty + end + + it 'fails closed when no official MPK proves the requested module identity' do + allow(@api).to receive(:search).and_return([]) + plan = @installer.dependencies('RootModule', api: @api) + expect(plan).not_to be_safe + expect(plan.blockers).to include( + 'DependencyModule: no official Marketplace package matched the module identity' + ) + expect { plan.apply! }.to raise_error(Mxrb::MarketplaceError, /blocked/) + end + + it 'ignores a failed candidate when a later verified candidate matches' do + allow(@api).to receive(:search) do |name:, **| + next [] unless name.delete(' _').casecmp('DependencyModule').zero? + + [{ 'contentId' => 99, 'type' => 'Module' }, { 'contentId' => 2, 'type' => 'Module' }] + end + allow(@api).to receive(:resolve) do |id, **| + raise Mxrb::MarketplaceError, 'candidate unavailable' if id == 99 + + @packages.fetch(id) + end + plan = @installer.dependencies('RootModule', api: @api) + expect(plan.blockers).to include( + 'TransitiveModule: no official Marketplace package matched the module identity' + ) + expect(plan.blockers.join(' ')).not_to include('candidate unavailable') + expect(plan.changes).to eq(['install DependencyModule 1.0.0']) + end + + it 'rejects candidates without a compatible package or matching module identity' do + allow(@api).to receive(:search).and_return([{ 'contentId' => 2, 'type' => 'Module' }]) + allow(@api).to receive(:resolve).and_return(nil) + expect(@installer.dependencies('RootModule', api: @api).blockers).to include( + 'DependencyModule: no official Marketplace package matched the module identity' + ) + + allow(@api).to receive(:search).and_return([{ 'contentId' => 3, 'type' => 'Module' }]) + allow(@api).to receive(:resolve).and_return(@packages.fetch(3)) + expect(@installer.dependencies('RootModule', api: @api).blockers).to include( + 'DependencyModule: no official Marketplace package matched the module identity' + ) + end + + it 'applies only verified packages with an explicit partial opt-in' do + allow(@api).to receive(:search) do |name:, **| + next [] unless name.delete(' _').casecmp('DependencyModule').zero? + + [{ 'contentId' => 2, 'type' => 'Module' }] + end + plan = @installer.dependencies('RootModule', api: @api, apply_resolved: true) + expect(plan).to have_attributes(safe?: false, applied?: true) + expect(plan.blockers).to include( + 'TransitiveModule: no official Marketplace package matched the module identity' + ) + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages').keys) + .to contain_exactly('RootModule', 'DependencyModule') + expect { plan.apply_resolved! }.to raise_error(Mxrb::MarketplaceError, /already applied/) + + preview = @installer.dependencies('RootModule', api: @api) + expect { preview.apply_resolved! }.to raise_error(Mxrb::MarketplaceError, /preview expired/) + end + + it 'rolls back dependencies already installed when a later import fails' do + calls = 0 + allow(@installer).to receive(:import_module).and_wrap_original do |original, *args| + calls += 1 + raise Errno::ENOSPC, 'disk full' if calls == 2 + + original.call(*args) + end + expect { @installer.dependencies('RootModule', api: @api, apply: true) } + .to raise_error(Errno::ENOSPC) + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages').keys).to eq(['RootModule']) + Mxrb.open(@target) { expect(_1.modules.map(&:name)).to contain_exactly('Host', 'RootModule') } + end + + it 'rejects non-module search results, wrong module identities, and paths outside the target' do + wrong = { 'contentId' => 3, 'type' => 'Widget' } + allow(@api).to receive(:search).and_return([wrong]) + plan = @installer.dependencies('RootModule', api: @api) + expect(plan).not_to be_safe + + resolver = described_class.new( + target: @root, mpr: @target, installer: @installer, api: @api, + mendix_version: '11.12.1' + ) + expect { resolver.send(:safe_path, '../outside') } + .to raise_error(Mxrb::MarketplaceError, /outside marketplace target/) + expect(resolver.send(:queries, 'Atlas_Core')).to eq(['Atlas Core', 'Atlas_Core']) + expect(resolver.send(:queries, 'Encryption')).to eq(['Encryption']) + expect(described_class::PLATFORM_MODULES).to eq(['System']) + end + + it 'uses a verified official content ID when the API name index differs from the module name' do + resolver = described_class.new( + target: @root, mpr: @target, installer: @installer, api: @api, + mendix_version: '11.12.1' + ) + content = { 'contentId' => 205_506, 'type' => 'Module' } + allow(@api).to receive(:content).with(205_506).and_return(content) + allow(@api).to receive(:search).and_return([]) + + expect(resolver.send(:candidates, 'feedbackmodule')).to eq([content]) + expect(@api).to have_received(:content).with(205_506) + end + + it 'short-circuits cycles and platform/self references and reuses a downloaded archive' do + resolver = described_class.new( + target: @root, mpr: @target, installer: @installer, api: @api, + mendix_version: '11.12.1' + ) + allow(resolver).to receive(:dependency_names).and_return(%w[System RootModule]) + resolver.send(:visit, 'RootModule', @archives.fetch(1)) + resolver.send(:visit, 'RootModule', @archives.fetch(1)) + expect(@api).not_to have_received(:search) + + Dir.mktmpdir do |temporary| + resolver.instance_variable_set(:@temporary, temporary) + first = resolver.send(:download_candidate, @packages.fetch(2)) + expect(resolver.send(:download_candidate, @packages.fetch(2))).to eq(first) + end + expect(@api).to have_received(:download).once + end + + it 'reports malformed dependency archives as Marketplace errors' do + resolver = described_class.new( + target: @root, mpr: @target, installer: @installer, api: @api, + mendix_version: '11.12.1' + ) + expect { resolver.send(:extract_project, @target, @root) } + .to raise_error(Mxrb::MarketplaceError, /invalid Mendix dependency package/) + end + + it 'accepts a host-owned dependency module without pretending it is a Marketplace package' do + Mxrb.define(@target) do + mendix_version '11.12.1' + self.module(:DependencyModule) { microflow :Work } + end + plan = @installer.dependencies('RootModule', api: @api) + expect(plan).to be_safe + expect(plan.changes).to be_empty + expect(@api).not_to have_received(:search) + end +end +# rubocop:enable Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/official_marketplace_lifecycle_spec.rb b/spec/official_marketplace_lifecycle_spec.rb new file mode 100644 index 0000000..b6b9465 --- /dev/null +++ b/spec/official_marketplace_lifecycle_spec.rb @@ -0,0 +1,409 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'tmpdir' +require 'zip' + +# rubocop:disable Metrics/BlockLength, Metrics/MethodLength +RSpec.describe Mxrb::OfficialMarketplace::Lifecycle do + def create_project(path, module_name:) + Mxrb.define(path) do + mendix_version '11.12.1' + self.module module_name do + entity(:Item) { string :Name } + microflow :Process + end + end + end + + def create_package(path, source, version:, asset:, module_name: 'MarketplaceModule') + xml = <<~XML + + + + + + + + + XML + manifest = { + 'package' => { 'name' => module_name, 'version' => version, 'type' => 'Module' }, + 'model-version' => '11.12.1' + } + Zip::File.open(path, create: true) do |zip| + zip.add('project.mpr', source) + zip.get_output_stream('package.xml') { _1.write(xml) } + zip.get_output_stream('manifest.json') { _1.write(JSON.generate(manifest)) } + zip.get_output_stream('widgets/lifecycle.mpk') { _1.write(asset) } + end + path + end + + def official_package(version) + Mxrb::OfficialMarketplace::OfficialPackage.new( + 'Marketplace Module', version, :mendix, nil, 'content:170', 170, + "version-#{version}", 'Module', 'Regular', [], false, true + ) + end + + around do |example| + Dir.mktmpdir do |dir| + @root = File.join(dir, 'project') + FileUtils.mkdir_p(@root) + @target = File.join(@root, 'App.mpr') + @source_v1 = File.join(dir, 'source-v1.mpr') + @source_v2 = File.join(dir, 'source-v2.mpr') + create_project(@target, module_name: :Host) + create_project(@source_v1, module_name: :MarketplaceModule) + FileUtils.cp(@source_v1, @source_v2) + Mxrb.open(@source_v2, readonly: false) do |project| + unit = project.find_artifact('MarketplaceModule.Process') + document = project.parse_bson(project.raw_unit(unit.unit_id)) + document['Name'] = 'ProcessV2' + project.mpr.update_unit(unit.unit_id, document) + end + @package_v1 = create_package( + File.join(dir, 'v1.mpk'), @source_v1, version: '1.0.0', asset: 'one' + ) + @package_v2 = create_package( + File.join(dir, 'v2.mpk'), @source_v2, version: '2.0.0', asset: 'two' + ) + @installer = Mxrb::OfficialMarketplace::Installer.new(target: @root, mpr: @target) + @installation = @installer.send( + :import_module, @package_v1, official_package('1.0.0'), @target + ) + @lifecycle = described_class.new(target: @root, mpr: @target, installer: @installer) + example.run + end + end + + it 'previews and explicitly applies a complete module removal' do + plan = @installer.remove('170') + expect(plan).to be_safe + expect(plan).not_to be_applied + expect(plan.changes).to include(match(/delete 3 MPR units/), match(/delete 1 package assets/)) + + plan.apply! + expect(plan).to be_applied + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages')).to be_empty + expect(File).not_to exist(File.join(@root, 'widgets', 'lifecycle.mpk')) + expect(File).not_to exist(@installation.archive) + Mxrb.open(@target) do |project| + expect(project.modules.map(&:name)).to eq(['Host']) + end + expect { plan.apply! }.to raise_error(Mxrb::MarketplaceError, /already applied/) + end + + it 'blocks removal when another unit references the module or an asset changed' do + Mxrb.open(@target, readonly: false) do |project| + root = project.mpr.root_unit + document = project.mpr.parse_contents(root) + document['LifecycleReference'] = @installation.module_id + project.mpr.update_unit(root.fetch('UnitID'), document) + end + plan = @lifecycle.plan_remove('MarketplaceModule') + expect(plan).not_to be_safe + expect(plan.blockers.join(' ')).to include('references 1 unit') + expect { plan.apply! }.to raise_error(Mxrb::MarketplaceError, /blocked/) + + File.write(File.join(@root, 'widgets', 'lifecycle.mpk'), 'locally changed') + expect(@lifecycle.plan_remove('MarketplaceModule').blockers.join(' ')) + .to include('package asset changed or missing') + FileUtils.rm_f(File.join(@root, 'widgets', 'lifecycle.mpk')) + expect(@lifecycle.plan_remove('MarketplaceModule').blockers.join(' ')) + .to include('package asset changed or missing') + external = File.join(File.dirname(@root), 'external-asset') + File.write(external, 'one') + FileUtils.ln_s(external, File.join(@root, 'widgets', 'lifecycle.mpk')) + expect(@lifecycle.plan_remove('MarketplaceModule').blockers.join(' ')) + .to include('package asset changed or missing') + expect { @installer.send(:preserve_asset, 'widgets/lifecycle.mpk', 'MarketplaceModule') } + .to raise_error(Mxrb::MarketplaceError, /symbolic link/) + + staged = File.join(File.dirname(@root), 'staged-asset') + File.write(staged, 'replacement') + assets = Mxrb::OfficialMarketplace::ModulePackageAssets.new(@root, File.dirname(@root)) + expect { assets.install('widgets/lifecycle.mpk' => staged) } + .to raise_error(Mxrb::MarketplaceError, /symbolic link/) + end + + it 'updates in place while preserving module identity and replacing assets and lock metadata' do + plan = @lifecycle.plan_update('MarketplaceModule', @package_v2, official_package('2.0.0')) + expect(plan).to have_attributes( + safe?: true, applied?: false, installed_version: '1.0.0', target_version: '2.0.0' + ) + plan.apply! + + expect(File.read(File.join(@root, 'widgets', 'lifecycle.mpk'))).to eq('two') + lock = Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'MarketplaceModule') + expect(lock).to include('version' => '2.0.0', 'version_id' => 'version-2.0.0') + expect(File).not_to exist(@installation.archive) + expect(Mxrb::OfficialMarketplace.verify(@root).dig('MarketplaceModule', :valid)).to be(true) + Mxrb.open(@target) do |project| + expect(project.find_artifact('MarketplaceModule.ProcessV2')).not_to be_nil + end + end + + it 'downloads an authenticated official update for preview and applies only when requested' do + package = official_package('2.0.0') + api = instance_double(Mxrb::OfficialMarketplace::ContentApi, resolve: package) + allow(api).to receive(:download) do |_version_id, destination, **| + FileUtils.cp(@package_v2, destination) + destination + end + + preview = @installer.update_official('MarketplaceModule', api:) + expect(preview).to have_attributes(safe?: true, applied?: false) + expect(Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'MarketplaceModule', 'version')) + .to eq('1.0.0') + + applied = @installer.update_official('170', api:, apply: true) + expect(applied).to have_attributes(safe?: true, applied?: true) + expect(Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'MarketplaceModule', 'version')) + .to eq('2.0.0') + end + + it 'restores MPR, assets, cache, and lock when an update fails after deletion' do + plan = @lifecycle.plan_update('MarketplaceModule', @package_v2, official_package('2.0.0')) + allow(@installer).to receive(:import_module).and_raise(Errno::ENOSPC, 'disk full') + expect { plan.apply! }.to raise_error(Errno::ENOSPC) + + expect(File.read(File.join(@root, 'widgets', 'lifecycle.mpk'))).to eq('one') + expect(File).to exist(@installation.archive) + expect(Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'MarketplaceModule', 'version')) + .to eq('1.0.0') + expect(Mxrb::OfficialMarketplace.verify(@root).dig('MarketplaceModule', :valid)).to be(true) + end + + it 'fails closed for identity changes, no-op versions, mismatched MPRs, and unknown packages' do + changed = @lifecycle.plan_update('MarketplaceModule', @package_v2, official_package('1.0.0')) + expect(changed.blockers).to include('version 1.0.0 is already installed') + expect { @lifecycle.installed('missing') } + .to raise_error(Mxrb::MarketplaceError, /not installed/) + + other = described_class.new(target: @root, mpr: File.join(@root, 'Other.mpr'), installer: @installer) + expect { other.plan_remove('MarketplaceModule') } + .to raise_error(Mxrb::MarketplaceError, /does not match/) + end + + it 'rejects replacement packages whose module name, identity, or manifest version changed' do + fresh_source = File.join(File.dirname(@source_v1), 'fresh.mpr') + create_project(fresh_source, module_name: :MarketplaceModule) + fresh_package = create_package( + File.join(File.dirname(@package_v1), 'fresh.mpk'), fresh_source, + version: '2.0.0', asset: 'fresh' + ) + identity = @lifecycle.plan_update( + 'MarketplaceModule', fresh_package, official_package('2.0.0') + ) + expect(identity.blockers).to include('replacement module identity changed') + + renamed_source = File.join(File.dirname(@source_v1), 'renamed.mpr') + create_project(renamed_source, module_name: :RenamedModule) + renamed_package = create_package( + File.join(File.dirname(@package_v1), 'renamed.mpk'), renamed_source, + version: '2.0.0', asset: 'renamed', module_name: 'RenamedModule' + ) + renamed = @lifecycle.plan_update( + 'MarketplaceModule', renamed_package, official_package('2.0.0') + ) + expect(renamed.blockers.join(' ')).to include('module name changed') + + mismatch = @lifecycle.plan_update( + 'MarketplaceModule', @package_v2, official_package('3.0.0') + ) + expect(mismatch.blockers.join(' ')).to include('downloaded package version 2.0.0') + end + + it 'fails closed for malformed locks, missing state, shared assets, and invalid packages' do + lock_path = File.join(@root, '.mxrb', 'marketplace.lock.json') + lock = Mxrb::OfficialMarketplace.lock(@root) + lock['packages']['TreePackage'] = { + 'destination' => 'modules/TreePackage', 'sha256' => 'x', + 'files' => ['widgets/lifecycle.mpk'] + } + File.write(lock_path, JSON.generate(lock)) + expect { @lifecycle.installed('TreePackage') } + .to raise_error(Mxrb::MarketplaceError, /not an imported module/) + expect(@lifecycle.plan_remove('MarketplaceModule').changes) + .to include('delete 0 package assets') + + expect { @lifecycle.send(:safe_path, '../outside') } + .to raise_error(Mxrb::MarketplaceError, /outside marketplace target/) + expect { @installer.send(:safe_target_path, '../outside') } + .to raise_error(Mxrb::MarketplaceError, /outside marketplace target/) + FileUtils.rm_f(@installation.archive) + expect { @lifecycle.plan_remove('MarketplaceModule') } + .to raise_error(Mxrb::MarketplaceError, /cached Marketplace package is missing/) + + FileUtils.cp(@package_v1, @installation.archive) + FileUtils.mv(@target, "#{@target}.saved") + expect { @lifecycle.plan_remove('MarketplaceModule') } + .to raise_error(Mxrb::MarketplaceError, /MPR not found/) + FileUtils.mv("#{@target}.saved", @target) + + lock = Mxrb::OfficialMarketplace.lock(@root) + lock['packages'].delete('TreePackage') + lock['packages']['MarketplaceModule']['module_id'] = SecureRandom.uuid + File.write(lock_path, JSON.generate(lock)) + expect { @lifecycle.plan_remove('MarketplaceModule') } + .to raise_error(Mxrb::MarketplaceError, /absent from target MPR/) + + host_only = File.join(File.dirname(@source_v1), 'host-only.mpr') + create_project(host_only, module_name: :HostOnly) + invalid = create_package( + File.join(File.dirname(@package_v1), 'absent.mpk'), host_only, + version: '2.0.0', asset: 'absent' + ) + expect { Mxrb::OfficialMarketplace::ModulePackageInventory.read(invalid) } + .to raise_error(Mxrb::MarketplaceError, /absent from package MPR/) + + broken_mpr = File.join(File.dirname(@source_v1), 'broken.mpr') + File.write(broken_mpr, 'not sqlite') + broken = create_package( + File.join(File.dirname(@package_v1), 'broken.mpk'), broken_mpr, + version: '2.0.0', asset: 'broken' + ) + expect { Mxrb::OfficialMarketplace::ModulePackageInventory.read(broken) } + .to raise_error(Mxrb::Error) + invalid_zip = File.join(File.dirname(@package_v1), 'invalid.zip') + File.write(invalid_zip, 'not a zip') + expect { Mxrb::OfficialMarketplace::ModulePackageInventory.read(invalid_zip) } + .to raise_error(Mxrb::MarketplaceError, /invalid Mendix module package/) + end + + it 'covers conservative lock blockers and complete file-or-directory rollback' do + entry = Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'MarketplaceModule') + inventory = Mxrb::OfficialMarketplace::ModulePackageInventory.read(@installation.archive) + mpr = Mxrb::IO::MprFile.open(@target, readonly: true) + host = mpr.units_by_containment('Modules').find { mpr.parse_contents(_1)['Name'] == 'Host' } + host_id = host.fetch('UnitID') + mpr.close + changed = entry.merge('module_id' => host_id, 'units' => 999) + blockers = @lifecycle.send(:base_blockers, changed, inventory, ['one']) + expect(blockers).to contain_exactly( + 'locked module identity does not match cached package', + 'locked unit count does not match target module tree' + ) + + directory = File.join(@root, 'rollback-directory') + file = File.join(@root, 'rollback-file') + missing = File.join(@root, 'rollback-missing') + FileUtils.mkdir_p(directory) + File.write(File.join(directory, 'value'), 'before') + File.write(file, 'before') + expect do + @lifecycle.send(:with_rollback, [directory, file, missing]) do + File.write(File.join(directory, 'value'), 'after') + File.write(file, 'after') + File.write(missing, 'created') + raise 'rollback' + end + end.to raise_error('rollback') + expect(File.read(File.join(directory, 'value'))).to eq('before') + expect(File.read(file)).to eq('before') + expect(File).not_to exist(missing) + end + + it 'removes the Atlas variables import only when its managed file exists' do + expect(@lifecycle.send(:remove_atlas_variables)).to be_nil + path = File.join(@root, 'theme', 'web', 'custom-variables.scss') + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, "before\n#{Mxrb::OfficialMarketplace::Installer::ATLAS_VARIABLES_IMPORT}\nafter\n") + @lifecycle.send(:remove_atlas_variables) + expect(File.read(path)).to eq("before\nafter\n") + end + + it 'covers alias, Atlas, equal-cache, and unopened-MPR refusal branches' do + lock_path = File.join(@root, '.mxrb', 'marketplace.lock.json') + lock = Mxrb::OfficialMarketplace.lock(@root) + lock['packages']['Alias'] = lock['packages'].delete('MarketplaceModule') + File.write(lock_path, JSON.generate(lock)) + expect(@lifecycle.plan_remove('Alias').blockers.join(' ')) + .to include('locked module name does not match cached package') + + lifecycle = described_class.new(target: @root, mpr: @target, installer: @installer) + allow(lifecycle).to receive(:with_rollback).and_yield + allow(lifecycle).to receive(:delete_units) + allow(lifecycle).to receive(:safe_path).and_return(File.join(@root, 'absent')) + allow(lifecycle).to receive(:delete_lock_entry) + expect(lifecycle).to receive(:remove_atlas_variables) + lifecycle.send(:apply_remove, 'Atlas_Core', { 'archive' => 'archive' }, @target, [], []) + + inventory = Mxrb::OfficialMarketplace::ModulePackageInventory.read(@package_v1) + entry = { 'archive' => '.mxrb/marketplace/MarketplaceModule-1.0.0.mpk', 'files' => [] } + allow(lifecycle).to receive(:safe_path).and_call_original + allow(@installer).to receive(:import_module) + lifecycle.send( + :apply_update, entry, @target, [], [], @package_v1, official_package('1.0.0'), inventory + ) + + unopened = described_class.new(target: @root, mpr: @target, installer: @installer) + expect { unopened.send(:delete_units, File.join(@root, 'missing.mpr'), []) } + .to raise_error(Mxrb::Error) + + expect(unopened.send(:package_version_matches?, '', '2.0.0')).to be(true) + expect(unopened.send(:package_version_matches?, 'unknown', '2.0.0')).to be(true) + expect(unopened.send(:package_version_matches?, '2.0.0', '2.0.0')).to be(true) + expect(unopened.send(:package_version_matches?, '1.0.0', '2.0.0')).to be(false) + end + + it 'applies removal directly through the installer opt-in' do + plan = @installer.remove('MarketplaceModule', apply: true) + expect(plan).to have_attributes(safe?: true, applied?: true) + end + + it 'restores an asset that existed before the package was installed' do + root = File.join(File.dirname(@source_v1), 'preexisting-project') + target = File.join(root, 'App.mpr') + asset = File.join(root, 'widgets', 'lifecycle.mpk') + FileUtils.mkdir_p(File.dirname(asset)) + create_project(target, module_name: :Host) + File.write(asset, 'user original') + installer = Mxrb::OfficialMarketplace::Installer.new(target: root, mpr: target) + installation = installer.send( + :import_module, @package_v1, official_package('1.0.0'), target + ) + entry = Mxrb::OfficialMarketplace.lock(root).dig('packages', 'MarketplaceModule') + backup = File.join(root, entry.dig('asset_originals', 'widgets/lifecycle.mpk')) + expect(File.read(asset)).to eq('one') + expect(File.read(backup)).to eq('user original') + + lifecycle = described_class.new(target: root, mpr: target, installer: installer) + lifecycle.plan_update( + 'MarketplaceModule', @package_v2, official_package('2.0.0') + ).apply! + expect(File.read(asset)).to eq('two') + expect(File.read(backup)).to eq('user original') + + plan = installer.remove('MarketplaceModule', apply: true) + expect(plan).to be_applied + expect(File.read(asset)).to eq('user original') + expect(File).not_to exist(backup) + expect(File).not_to exist(installation.archive) + end + + it 'cleans a newly persisted original-asset backup when import fails' do + root = File.join(File.dirname(@source_v1), 'failed-project') + target = File.join(root, 'App.mpr') + asset = File.join(root, 'widgets', 'lifecycle.mpk') + FileUtils.mkdir_p(File.dirname(asset)) + create_project(target, module_name: :Host) + File.write(asset, 'user original') + installer = Mxrb::OfficialMarketplace::Installer.new(target: root, mpr: target) + importer = double + allow(importer).to receive(:import!).and_raise(Errno::ENOSPC, 'disk full') + allow(installer).to receive(:module_importer).and_return(importer) + + expect do + installer.send(:import_module, @package_v1, official_package('1.0.0'), target) + end.to raise_error(Errno::ENOSPC) + backup = File.join( + root, '.mxrb', 'marketplace-originals', 'MarketplaceModule', 'widgets', 'lifecycle.mpk' + ) + expect(File).not_to exist(backup) + expect(File.read(asset)).to eq('user original') + end +end +# rubocop:enable Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/official_marketplace_widget_spec.rb b/spec/official_marketplace_widget_spec.rb new file mode 100644 index 0000000..bc73652 --- /dev/null +++ b/spec/official_marketplace_widget_spec.rb @@ -0,0 +1,613 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'tmpdir' +require 'zip' + +# rubocop:disable Metrics/BlockLength, Metrics/MethodLength, Metrics/ParameterLists +RSpec.describe Mxrb::OfficialMarketplace::WidgetPackageInstaller do + def widget_package(path, version: '2.9.0', declared: 'com/mendix/widget/web/combobox/', + widget_id: 'com.mendix.widget.web.combobox.Combobox', name: 'Combobox', + runtime: true) + package = <<~XML + + + + + + + + XML + widget = <<~XML + + + Combo box + + XML + Zip::File.open(path, create: true) do |zip| + zip.get_output_stream('package.xml') { _1.write(package) } + zip.get_output_stream('Combobox.xml') { _1.write(widget) } + runtime_path = File.join(declared.delete_suffix('/'), "#{name}.mjs") + zip.get_output_stream(runtime_path) { _1.write('export {};') } if runtime + end + path + end + + def data_widgets_package(path, source, nested_widget) + package = <<~XML + + + + + + + + + XML + manifest = { + 'package' => { 'name' => 'DataWidgets', 'version' => '3.11.3', 'type' => 'Module' }, + 'model-version' => '11.12.1' + } + Zip::File.open(path, create: true) do |zip| + zip.add('project.mpr', source) + zip.add('widgets/com.mendix.widget.web.Datagrid.mpk', nested_widget) + zip.get_output_stream('package.xml') { _1.write(package) } + zip.get_output_stream('manifest.json') { _1.write(JSON.generate(manifest)) } + end + path + end + + def module_widget_package(path, source, nested_widget) + package = <<~XML + + + + + + + + + XML + manifest = { + 'package' => { 'name' => 'Atlas_Core', 'version' => '4.3.7', 'type' => 'Module' }, + 'model-version' => '11.12.1' + } + Zip::File.open(path, create: true) do |zip| + zip.add('project.mpr', source) + zip.add('widgets/com.mendix.widget.web.Combobox.mpk', nested_widget) + zip.get_output_stream('package.xml') { _1.write(package) } + zip.get_output_stream('manifest.json') { _1.write(JSON.generate(manifest)) } + end + path + end + + def official_package(type: 'Widget', version: '2.9.0') + Mxrb::OfficialMarketplace::OfficialPackage.new( + 'Combo box', version, :mendix, 'https://marketplace-api.test/download', + 'content:219304', 219_304, '123e4567-e89b-12d3-a456-426614174000', + type, 'Regular', [], false, true + ) + end + + def archive_with_entries(name, entries) + path = File.join(@root, name) + Zip::File.open(path, create: true) do |zip| + entries.each { |entry, contents| zip.get_output_stream(entry) { _1.write(contents) } } + end + path + end + + def write_marketplace_lock(packages) + path = File.join(@root, '.mxrb', 'marketplace.lock.json') + FileUtils.mkdir_p(File.dirname(path)) + File.write(path, JSON.generate('packages' => packages)) + end + + def client_package_xml(name:, version:, widgets:) + declarations = widgets.map { %() }.join + "#{declarations}" + end + + before do + @root = Dir.mktmpdir('mxrb-widget-install-') + @archive = widget_package(File.join(@root, 'source.mpk')) + end + + after { FileUtils.remove_entry(@root) } + + it 'installs an authenticated official widget as an opaque project MPK and verifies both copies' do + result = described_class.new(target: @root).install(@archive, official_package) + expect(result).to have_attributes( + widget_name: 'Combobox', + widget_ids: ['com.mendix.widget.web.combobox.Combobox'], + destination: File.join(@root, 'widgets', 'com.mendix.widget.web.Combobox.mpk') + ) + expect(Digest::SHA256.file(result.destination).hexdigest).to eq(result.sha256) + expect(Digest::SHA256.file(result.archive).hexdigest).to eq(result.sha256) + + entry = Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'Combo box') + expect(entry).to include( + 'kind' => 'widget', 'content_id' => 219_304, 'version' => '2.9.0', + 'widget_name' => 'Combobox', + 'widget_ids' => ['com.mendix.widget.web.combobox.Combobox'] + ) + verification = Mxrb::OfficialMarketplace.verify(@root).fetch('Combo box') + expect(verification[:valid]).to be(true) + + File.binwrite(result.destination, 'changed') + expect(Mxrb::OfficialMarketplace.verify(@root).dig('Combo box', :valid)).to be(false) + end + + it 'downloads Widget content with the authenticated API and routes it by package structure' do + api = instance_double(Mxrb::OfficialMarketplace::ContentApi, resolve: official_package) + allow(api).to receive(:download) do |_version_id, destination, **| + FileUtils.cp(@archive, destination) + destination + end + mpr = File.join(@root, 'App.mpr') + File.write(mpr, 'mpr boundary is detected separately') + installer = Mxrb::OfficialMarketplace::Installer.new(target: @root, mpr:) + allow(installer).to receive(:detect_mendix_version).and_return('11.12.1') + + result = installer.pull_official('219304', api:) + expect(result).to be_a(Mxrb::OfficialMarketplace::WidgetInstallation) + expect(api).to have_received(:download).with( + official_package.version_id, end_with('package.mpk'), download_url: official_package.download_url + ) + end + + it 'fails closed on metadata/structure and version identity mismatches' do + installer = Mxrb::OfficialMarketplace::Installer.new(target: @root) + expect do + installer.send(:install_official_archive, @archive, official_package(type: 'Module'), __FILE__) + end.to raise_error(Mxrb::MarketplaceError, /Module archive declares a clientModule/) + expect do + described_class.new(target: @root).install(@archive, official_package(version: '3.0.0')) + end.to raise_error(Mxrb::MarketplaceError, /does not match Marketplace/) + unsupported = official_package(type: 'Template') + expect { installer.validate_official_package!(unsupported) } + .to raise_error(Mxrb::MarketplaceError, /not a Module, Service, or Widget/) + allow(Mxrb::OfficialMarketplace::PackageEnvelope).to receive(:kind).and_return(:unknown) + expect { installer.send(:install_official_archive, @archive, official_package, __FILE__) } + .to raise_error(Mxrb::MarketplaceError, /unsupported official/) + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages')).to be_empty + expect(Dir.glob(File.join(@root, 'widgets', '*.mpk'))).to be_empty + end + + it 'rejects incomplete, divergent, and unsafe widget envelopes without residue' do + missing = widget_package( + File.join(@root, 'missing.mpk'), declared: 'missing/runtime/', runtime: false + ) + expect { described_class.new(target: @root).install(missing, official_package) } + .to raise_error(Mxrb::MarketplaceError, /declared widget file is missing/) + + divergent = widget_package(File.join(@root, 'divergent.mpk'), widget_id: 'invalid') + expect { described_class.new(target: @root).install(divergent, official_package) } + .to raise_error(Mxrb::MarketplaceError, /invalid widget ID/) + + unsafe = File.join(@root, 'unsafe.mpk') + Zip::File.open(unsafe, create: true) do |zip| + zip.get_output_stream('../package.xml') { _1.write('') } + end + expect { Mxrb::OfficialMarketplace::WidgetPackageInventory.read(unsafe) } + .to raise_error(Mxrb::MarketplaceError, /unsafe widget package path/) + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages')).to be_empty + end + + it 'rejects malformed package envelopes and every unproven widget identity boundary' do + envelope = Mxrb::OfficialMarketplace::PackageEnvelope + inventory = Mxrb::OfficialMarketplace::WidgetPackageInventory + missing = archive_with_entries('no-package.mpk', 'readme.txt' => 'x') + unknown = archive_with_entries('unknown.mpk', 'package.xml' => '') + rootless = archive_with_entries('rootless.mpk', 'package.xml' => '') + broken = File.join(@root, 'broken.mpk') + File.write(broken, 'not zip') + expect { envelope.kind(missing) }.to raise_error(Mxrb::MarketplaceError, /package.xml is missing/) + expect { envelope.kind(unknown) }.to raise_error(Mxrb::MarketplaceError, /does not declare/) + expect { envelope.kind(rootless) }.to raise_error(Mxrb::MarketplaceError, /does not declare/) + expect { envelope.kind(broken) }.to raise_error(Mxrb::MarketplaceError, /invalid Mendix package/) + expect { inventory.read(missing) }.to raise_error(Mxrb::MarketplaceError, /package.xml is missing/) + expect { inventory.read(unknown) }.to raise_error(Mxrb::MarketplaceError, /exactly one clientModule/) + + no_widgets = archive_with_entries('no-widgets.mpk', { + 'package.xml' => '' + }) + expect { inventory.read(no_widgets) }.to raise_error(Mxrb::MarketplaceError, /does not declare widgetFiles/) + + invalid_name = archive_with_entries('invalid-name.mpk', { + 'package.xml' => client_package_xml(name: '123', version: '1', widgets: ['A.xml']), + 'A.xml' => '' + }) + expect { inventory.read(invalid_name) }.to raise_error(Mxrb::MarketplaceError, /clientModule name/) + invalid_version = archive_with_entries('invalid-version.mpk', { + 'package.xml' => client_package_xml(name: 'A', version: '', widgets: ['A.xml']), + 'A.xml' => '' + }) + expect { inventory.read(invalid_version) }.to raise_error(Mxrb::MarketplaceError, /clientModule version/) + missing_definition = archive_with_entries('missing-definition.mpk', { + 'package.xml' => client_package_xml(name: 'A', version: '1', widgets: ['Missing.xml']) + }) + expect { inventory.read(missing_definition) }.to raise_error(Mxrb::MarketplaceError, /definition is missing/) + non_xml = archive_with_entries('non-xml.mpk', { + 'package.xml' => client_package_xml(name: 'A', version: '1', widgets: ['A.js']), + 'A.js' => 'x' + }) + expect { inventory.read(non_xml) }.to raise_error(Mxrb::MarketplaceError, /is not XML/) + malformed = archive_with_entries('malformed.mpk', 'package.xml' => ' client_package_xml(name: 'A', version: '1', widgets: ['A.xml']), + 'A.xml' => '', + 'a.XML' => '' + }) + expect { inventory.read(duplicate) }.to raise_error(Mxrb::MarketplaceError, /duplicate widget package path/) + + linked = double(name: 'linked.xml', directory?: false, symlink?: true) + fake_archive = double + allow(fake_archive).to receive(:each).and_yield(linked) + expect { inventory.send(:validate_entries!, fake_archive) } + .to raise_error(Mxrb::MarketplaceError, /symbolic link/) + + wrong_root = archive_with_entries('wrong-root.mpk', { + 'package.xml' => client_package_xml(name: 'A', version: '1', widgets: ['A.xml']), + 'A.xml' => '' + }) + expect { inventory.read(wrong_root) }.to raise_error(Mxrb::MarketplaceError, /invalid widget ID/) + rootless_widget = archive_with_entries('rootless-widget.mpk', { + 'package.xml' => client_package_xml(name: 'A', version: '1', widgets: ['A.xml']), + 'A.xml' => '' + }) + expect { inventory.read(rootless_widget) }.to raise_error(Mxrb::MarketplaceError, /invalid widget ID/) + + divergent = archive_with_entries('namespaces.mpk', { + 'package.xml' => client_package_xml(name: 'A', version: '1', widgets: %w[A.xml B.xml]), + 'A.xml' => '', + 'B.xml' => '' + }) + expect { inventory.read(divergent) }.to raise_error(Mxrb::MarketplaceError, /do not share/) + expect(inventory.send(:elements, nil, 'widget')).to eq([]) + expect { inventory.send(:safe_relative_path, "bad\0.xml") } + .to raise_error(Mxrb::MarketplaceError, /unsafe/) + end + + it 'rolls back widget, cache, and lock when the transaction cannot complete' do + installer = described_class.new(target: @root) + allow(installer).to receive(:write_lock).and_raise(Mxrb::MarketplaceError, 'lock failed') + expect { installer.install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /lock failed/) + expect(Dir.glob(File.join(@root, 'widgets', '*.mpk'))).to be_empty + expect(Dir.glob(File.join(@root, '.mxrb', 'marketplace', '*.mpk'))).to be_empty + expect(File).not_to exist(File.join(@root, '.mxrb', 'marketplace.lock.json')) + end + + it 'rejects existing and symlinked destinations before writing a lock or cache' do + widgets = File.join(@root, 'widgets') + external = Dir.mktmpdir('mxrb-widget-external-') + File.symlink(external, widgets) + expect { described_class.new(target: @root).install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /symbolic link/) + expect(Dir.children(external)).to be_empty + ensure + FileUtils.remove_entry(external) if external && File.directory?(external) + end + + it 'fails closed on unsafe target, cache, lock, checksum, and installed-widget states' do + missing = File.join(@root, 'missing') + expect { described_class.new(target: missing).install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /target not found/) + + external = Dir.mktmpdir('mxrb-target-') + linked_target = File.join(@root, 'linked-target') + File.symlink(external, linked_target) + expect { described_class.new(target: linked_target).install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /target is a symbolic link/) + + destination = File.join(@root, 'widgets', 'com.mendix.widget.web.Combobox.mpk') + FileUtils.mkdir_p(destination) + expect { described_class.new(target: @root).install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /destination is a directory/) + FileUtils.rm_rf(destination) + + FileUtils.mkdir_p(File.dirname(destination)) + linked_file = File.join(external, 'widget.mpk') + File.write(linked_file, 'widget') + File.symlink(linked_file, destination) + expect { described_class.new(target: @root).install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /destination is a symbolic link/) + FileUtils.rm_f(destination) + + cache = File.join(@root, '.mxrb', 'marketplace', 'com.mendix.widget.web.Combobox-2.9.0.mpk') + FileUtils.mkdir_p(File.dirname(cache)) + File.write(cache, 'occupied') + expect { described_class.new(target: @root).install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /cache destination already exists/) + FileUtils.rm_f(cache) + + installer = described_class.new(target: @root) + expect { installer.send(:verify_copy!, @archive, 'wrong') } + .to raise_error(Mxrb::MarketplaceError, /checksum mismatch/) + expect { installer.send(:safe_path, '../outside') } + .to raise_error(Mxrb::MarketplaceError, /outside marketplace target/) + expect(installer.send(:locked_asset_digest, { 'kind' => 'legacy' }, 'widgets/x.mpk')).to be_nil + expect(installer.send(:locked_asset_digest, { + 'kind' => 'widget', 'destination' => 'widgets/x.mpk', 'sha256' => 'digest' + }, 'widgets/x.mpk')).to eq('digest') + ensure + FileUtils.remove_entry(external) if external && File.directory?(external) + end + + it 'rejects corrupt or conflicting current widget locks and verifies corrupt archives safely' do + installer = described_class.new(target: @root) + destination = File.join(@root, 'widgets', 'com.mendix.widget.web.Combobox.mpk') + relative = 'widgets/com.mendix.widget.web.Combobox.mpk' + expect { installer.send(:validate_current!, ['Combo box', { 'kind' => 'module' }], official_package, destination) } + .to raise_error(Mxrb::MarketplaceError, /is not a widget/) + expect do + installer.send( + :validate_current!, ['Combo box', { 'kind' => 'widget', 'destination' => 'widgets/other.mpk' }], + official_package, destination + ) + end.to raise_error(Mxrb::MarketplaceError, /changed its destination/) + invalid = { + 'kind' => 'widget', 'destination' => relative, 'archive' => 'missing.mpk', + 'sha256' => 'x', 'widget_name' => 'Combobox', 'widget_ids' => [] + } + expect { installer.send(:validate_current!, ['Combo box', invalid], official_package, destination) } + .to raise_error(Mxrb::MarketplaceError, /failed verification/) + + installed = installer.install(@archive, official_package) + entry = Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'Combo box') + expect { installer.send(:validate_current!, ['Combo box', entry], official_package, destination) } + .to raise_error(Mxrb::MarketplaceError, /already installed/) + newer = official_package(version: '3.0.0') + lock_path = File.join(@root, '.mxrb', 'marketplace.lock.json') + expect(installer.send( + :validate_boundaries!, newer, destination, installed.archive, lock_path + )).to be_a(Array) + + entry['widget_name'] = 'Other' + expect(Mxrb::OfficialMarketplace.verify_widget(@root, entry)[:valid]).to be(false) + entry['widget_name'] = 'Combobox' + entry['widget_ids'] = [] + expect(Mxrb::OfficialMarketplace.verify_widget(@root, entry)[:valid]).to be(false) + entry['widget_ids'] = ['com.mendix.widget.web.combobox.Combobox'] + + missing_destination = entry.merge( + 'destination' => 'widgets/missing.mpk', 'widget_name' => nil, 'widget_ids' => [] + ) + expect(Mxrb::OfficialMarketplace.verify_widget(@root, missing_destination)[:valid]).to be(false) + + File.binwrite(installed.destination, 'not zip') + File.binwrite(installed.archive, 'not zip') + digest = Digest::SHA256.file(installed.destination).hexdigest + lock = Mxrb::OfficialMarketplace.lock(@root) + lock['packages']['Combo box']['sha256'] = digest + write_marketplace_lock(lock.fetch('packages')) + expect(Mxrb::OfficialMarketplace.verify(@root).dig('Combo box', :valid)).to be(false) + end + + it 'discovers a missing WidgetId through a verified official mapping and installs its bundle' do + datagrid_id = 'com.mendix.widget.web.datagrid.Datagrid' + source = File.join(@root, 'data-widgets.mpr') + target = File.join(@root, 'App.mpr') + Mxrb.define(source) do + mendix_version '11.12.1' + self.module(:DataWidgets) + end + Mxrb.define(target) do + mendix_version '11.12.1' + self.module(:App) + end + nested = widget_package( + File.join(@root, 'datagrid.mpk'), version: '3.11.3', + declared: 'com/mendix/widget/web/datagrid/', + widget_id: datagrid_id, name: 'Datagrid' + ) + bundle = data_widgets_package(File.join(@root, 'data-widgets.mpk'), source, nested) + package = Mxrb::OfficialMarketplace::OfficialPackage.new( + 'Data Widgets', '3.11.3', :mendix, 'https://marketplace-api.test/data-widgets', + 'content:116540', 116_540, '123e4567-e89b-12d3-a456-426614174001', + 'Module', 'Regular', [], false, true + ) + api = instance_double(Mxrb::OfficialMarketplace::ContentApi) + allow(api).to receive(:resolve).with(116_540, mendix_version: '11.12.1').and_return(package) + allow(api).to receive(:download) do |_version_id, destination, **| + FileUtils.cp(bundle, destination) + destination + end + installer = Mxrb::OfficialMarketplace::Installer.new(target: @root, mpr: target) + resolver = Mxrb::OfficialMarketplace::DependencyResolver.new( + target: @root, mpr: target, installer:, api:, mendix_version: '11.12.1' + ) + + Dir.mktmpdir do |temporary| + resolver.instance_variable_set(:@temporary, temporary) + resolver.instance_variable_set(:@required_widget_ids, [datagrid_id]) + dependencies = resolver.send(:resolve_widget_dependencies) + expect(dependencies.map(&:required_ids)).to eq([[datagrid_id]]) + expect(dependencies.first.widget_ids).to include(datagrid_id) + resolver.send(:apply_dependencies, [], dependencies) + end + + expect(Mxrb::WidgetPackage.find(@root, datagrid_id)).not_to be_nil + expect(Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'DataWidgets', 'content_id')) + .to eq(116_540) + expect(Mxrb::OfficialMarketplace.verify(@root).dig('DataWidgets', :valid)).to be(true) + end + + it 'reports unknown, unavailable, locked, and identity-mismatched WidgetId dependencies' do + target = File.join(@root, 'App.mpr') + Mxrb.define(target) do + mendix_version '11.12.1' + self.module(:App) + end + api = instance_double(Mxrb::OfficialMarketplace::ContentApi) + allow(api).to receive(:resolve) + installer = Mxrb::OfficialMarketplace::Installer.new(target: @root, mpr: target) + resolver = Mxrb::OfficialMarketplace::DependencyResolver.new( + target: @root, mpr: target, installer:, api:, mendix_version: '11.12.1' + ) + resolver.instance_variable_set(:@required_widget_ids, ['org.example.unknown.Widget']) + expect(resolver.send(:resolve_widget_dependencies)).to eq([]) + expect(resolver.instance_variable_get(:@blockers).join).to include('no verified official') + + datagrid_id = 'com.mendix.widget.web.datagrid.Datagrid' + resolver.instance_variable_set(:@required_widget_ids, [datagrid_id]) + allow(api).to receive(:resolve).and_raise(Mxrb::MarketplaceError, 'API unavailable') + expect(resolver.send(:resolve_widget_dependencies)).to eq([]) + expect(resolver.instance_variable_get(:@blockers).join).to include('API unavailable') + + write_marketplace_lock('Existing' => { 'content_id' => 116_540 }) + expect { resolver.send(:resolve_widget_dependency, 116_540, [datagrid_id]) } + .to raise_error(Mxrb::MarketplaceError, /locked official package/) + + write_marketplace_lock({}) + wrong = official_package + allow(api).to receive(:resolve).and_return(wrong) + Dir.mktmpdir do |temporary| + resolver.instance_variable_set(:@temporary, temporary) + allow(api).to receive(:download) do |_version_id, destination, **| + FileUtils.cp(@archive, destination) + destination + end + expect { resolver.send(:resolve_widget_dependency, 116_540, [datagrid_id]) } + .to raise_error(Mxrb::MarketplaceError, /does not provide/) + end + expect(resolver.send(:collect_widget_ids, { + 'WidgetId' => 'one.Widget', 'Nested' => [{ 'WidgetId' => 'two.Widget' }] + })).to contain_exactly('one.Widget', 'two.Widget') + expect(resolver.send(:collect_widget_ids, 'plain')).to eq([]) + end + + it 'reuses verified widget downloads, reports plan changes, and skips already installed content' do + target = File.join(@root, 'App.mpr') + Mxrb.define(target) do + mendix_version '11.12.1' + self.module(:App) + end + api = instance_double(Mxrb::OfficialMarketplace::ContentApi) + allow(api).to receive(:resolve) + installer = Mxrb::OfficialMarketplace::Installer.new(target: @root, mpr: target) + resolver = Mxrb::OfficialMarketplace::DependencyResolver.new( + target: @root, mpr: target, installer:, api:, mendix_version: '11.12.1' + ) + dependency = Mxrb::OfficialMarketplace::Dependency.new( + 'Unused', official_package, @archive, [] + ) + installed_dependency = Mxrb::OfficialMarketplace::Dependency.new( + 'Installed', nil, @archive, [] + ) + resolver.instance_variable_set( + :@resolved, 'Installed' => installed_dependency, 'Unused' => dependency + ) + combo_id = 'com.mendix.widget.web.combobox.Combobox' + resolved = resolver.send(:resolve_widget_dependency, 219_304, [combo_id]) + expect(resolved.widget_ids).to eq([combo_id]) + expect(api).not_to have_received(:resolve) + expect(resolver.send(:package_asset_paths, @archive)) + .to eq(['widgets/com.mendix.widget.web.Combobox.mpk']) + + plan = Mxrb::OfficialMarketplace::DependencyPlan.new( + root: 'App', dependencies: [], widget_dependencies: [resolved], blockers: [] + ) + expect(plan.changes).to eq(['install widget bundle Combo box 2.9.0']) + + write_marketplace_lock('Existing' => { 'content_id' => 219_304 }) + expect(installer).not_to receive(:install_official_archive) + resolver.send(:apply_dependencies, [], [resolved]) + end + + it 'fails closed when bundle enumeration cannot read a structurally selected module' do + allow(Mxrb::OfficialMarketplace::PackageEnvelope).to receive(:kind).and_return(:module) + allow(Mxrb::OfficialMarketplace::ModulePackageInventory).to receive(:read).and_return(double) + allow(Zip::File).to receive(:open).and_raise(Zip::Error, 'broken nested package') + expect { Mxrb::OfficialMarketplace::WidgetBundleInventory.read(@archive) } + .to raise_error(Mxrb::MarketplaceError, /invalid Mendix widget bundle/) + end + + it 'renames a content-ID matched widget lock during an official upgrade' do + installer = described_class.new(target: @root) + installer.install(@archive, official_package) + lock = Mxrb::OfficialMarketplace.lock(@root) + lock['packages']['Alias'] = lock['packages'].delete('Combo box') + write_marketplace_lock(lock.fetch('packages')) + newer_archive = widget_package(File.join(@root, 'combo-3.0.0.mpk'), version: '3.0.0') + installer.install(newer_archive, official_package(version: '3.0.0')) + expect(Mxrb::OfficialMarketplace.lock(@root).fetch('packages').keys).to eq(['Combo box']) + end + + it 'upgrades a module-owned widget transactionally and protects shared ownership' do + target = File.join(@root, 'App.mpr') + atlas_source = File.join(@root, 'Atlas.mpr') + Mxrb.define(target) do + mendix_version '11.12.1' + self.module(:App) + end + Mxrb.define(atlas_source) do + mendix_version '11.12.1' + self.module(:Atlas_Core) + end + old_widget = widget_package(File.join(@root, 'combo-2.6.2.mpk'), version: '2.6.2') + atlas_archive = module_widget_package( + File.join(@root, 'atlas.mpk'), atlas_source, old_widget + ) + atlas = Mxrb::OfficialMarketplace::OfficialPackage.new( + 'Atlas Core', '4.3.7', :mendix, nil, 'content:117187', 117_187, + '123e4567-e89b-12d3-a456-426614174002', 'Module', 'Regular', [], false, true + ) + module_installer = Mxrb::OfficialMarketplace::Installer.new(target: @root, mpr: target) + module_installer.send(:import_module, atlas_archive, atlas, target) + destination = File.join(@root, 'widgets', 'com.mendix.widget.web.Combobox.mpk') + old_digest = Digest::SHA256.file(destination).hexdigest + + widget_installer = described_class.new(target: @root) + File.binwrite(destination, 'locally changed') + expect { widget_installer.install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /owned widget asset changed/) + FileUtils.cp(old_widget, destination) + + atlas_entry = Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'Atlas_Core') + cached_atlas = File.join(@root, atlas_entry.fetch('archive')) + FileUtils.mv(cached_atlas, "#{cached_atlas}.held") + expect { widget_installer.install(@archive, official_package) } + .to raise_error(Mxrb::MarketplaceError, /cached Marketplace package is missing/) + FileUtils.mv("#{cached_atlas}.held", cached_atlas) + + current = widget_installer.install(@archive, official_package) + entry = Mxrb::OfficialMarketplace.lock(@root).dig('packages', 'Combo box') + backup = File.join(@root, entry.fetch('asset_original')) + expect(Digest::SHA256.file(backup).hexdigest).to eq(old_digest) + expect(entry.fetch('files')).to eq(['widgets/com.mendix.widget.web.Combobox.mpk']) + + newer_archive = widget_package(File.join(@root, 'combo-3.0.0.mpk'), version: '3.0.0') + newer = official_package(version: '3.0.0') + upgraded = widget_installer.install(newer_archive, newer) + expect(upgraded.sha256).not_to eq(current.sha256) + expect(Digest::SHA256.file(backup).hexdigest).to eq(old_digest) + expect(File).not_to exist(current.archive) + + failing_archive = widget_package(File.join(@root, 'combo-3.1.0.mpk'), version: '3.1.0') + failing = described_class.new(target: @root) + allow(failing).to receive(:write_lock).and_raise(Mxrb::MarketplaceError, 'lock failed') + expect { failing.install(failing_archive, official_package(version: '3.1.0')) } + .to raise_error(Mxrb::MarketplaceError, /lock failed/) + expect(Digest::SHA256.file(destination).hexdigest).to eq(upgraded.sha256) + expect(Mxrb::OfficialMarketplace.verify(@root).dig('Combo box', :valid)).to be(true) + + assets = Mxrb::OfficialMarketplace::ModulePackageAssets.new(@root, @root) + assets.install({ 'widgets/com.mendix.widget.web.Combobox.mpk' => old_widget }, [ + 'widgets/com.mendix.widget.web.Combobox.mpk' + ]) + expect(Digest::SHA256.file(destination).hexdigest).to eq(upgraded.sha256) + + removal = module_installer.remove('Atlas_Core', apply: true) + expect(removal).to be_safe + expect(Digest::SHA256.file(destination).hexdigest).to eq(upgraded.sha256) + expect(Mxrb::OfficialMarketplace.verify(@root).dig('Combo box', :valid)).to be(true) + end +end +# rubocop:enable Metrics/BlockLength, Metrics/MethodLength, Metrics/ParameterLists diff --git a/spec/page_bundle_compiler_spec.rb b/spec/page_bundle_compiler_spec.rb index c858fb0..961babb 100644 --- a/spec/page_bundle_compiler_spec.rb +++ b/spec/page_bundle_compiler_spec.rb @@ -37,6 +37,8 @@ it 'renders page content, layout metadata, and translated text as an ES module' do source = Mxrb::Compiler::SourceModel.read(@mpr) + allow(source).to receive(:units_of).and_call_original + allow(source).to receive(:documents).and_call_original unit = source.units_of('Forms$Page').first unit.document['Appearance'] = { 'Class' => 'page-identity' } bundle = described_class.new(source).compile(unit) @@ -668,6 +670,392 @@ 'PlaceholderTemplate' => { 'Template' => { 'Items' => [2] } } })).to include('"mode": "time"', '"timeFormat"') end + + it 'covers advanced standard widgets, menus, actions, lists, and import flags' do # rubocop:disable Metrics/BlockLength + source = Mxrb::Compiler::SourceModel.read(@mpr) + unit = source.units_of('Forms$Page').first + compiler = described_class.new(source) + compiler.send(:prepare_compile, unit, 'p') + compiler.instance_variable_set(:@data_view_scopes, [{ scope: 'p.Demo.Home.item', entity: 'Demo.Item' }]) + + expect(compiler.send(:render_widget, '$Type' => 'Forms$Title', 'Name' => 'title')).to include('h1') + expect(compiler.send(:render_widget, { + '$Type' => 'Forms$GroupBox', 'Name' => 'group', 'Collapsible' => 'YesInitiallyCollapsed', + 'HeaderMode' => 'H2', 'Widgets' => [] + })).to include('"collapsible": "yes"') + expect(compiler.send(:render_group_box, { + 'Name' => 'expanded', 'Collapsible' => 'YesInitiallyExpanded', 'Widgets' => [] + })).to include('"collapsible": "expanded"') + expect(compiler.send(:render_group_box, { 'Name' => 'plain', 'Widgets' => [] })) + .to include('"collapsible": "no"') + expect(compiler.send(:render_group_box, { + 'Name' => 'content', 'Widgets' => [2, { '$Type' => 'Forms$DynamicText', 'Name' => 'child' }] + })).to include('child') + + radio = { '$Type' => 'Forms$RadioButtonGroup', 'Name' => 'state', + 'AttributeRef' => { 'Attribute' => 'Demo.Item.State' } } + expect(compiler.send(:render_widget, radio)).to include('$RadioButtonGroup') + expect(compiler.send(:render_widget, { + '$Type' => 'Forms$FileManager', 'Name' => 'file', 'Type' => '', 'MaxFileSize' => 'invalid' + })).to include('$FileManager', '"widgetType": "both"') + expect(compiler.send(:render_text_area, { + 'Name' => 'description', 'AttributeRef' => { 'Attribute' => 'Demo.Item.Description' }, + 'AutoGrow' => true, 'MaxLengthCode' => 100, 'Autocomplete' => false, + 'SubmitBehaviour' => 'WhileEditing' + })).to include('$TextArea', '"autoGrow": true') + + expect(compiler.send(:menu_items, nil)).to be_nil + expect(compiler.send(:menu_items, '$Type' => 'Unknown')).to be_nil + expect(compiler.send(:menu_icon_property, nil)).to be_nil + expect(compiler.send(:menu_icon_property, 'Image' => 'Demo.Icons.AddItem')).to include('add-item') + expect(compiler.send(:grid_weight_class, 'md', 0)).to be_nil + expect(compiler.send(:grid_weight_class, 'md', 12)).to eq('col-md-12') + expect(compiler.send(:compile_menu_item, { + 'Caption' => { 'Items' => [2] }, 'Items' => [2, { 'Caption' => { 'Items' => [2] } }] + }, {}, [0])).to include(:items) + expect(compiler.send(:menu_microflow_config, '$Type' => 'Other')).to be_nil + expect(compiler.send(:menu_microflow_config, { + '$Type' => 'Forms$MicroflowAction', + 'MicroflowSettings' => { 'Microflow' => 'Demo.ServerAction', 'ParameterMappings' => [2] } + })).to include(type: 'callMicroflow') + + expect(compiler.send(:open_page_config, '$Type' => 'Forms$FormAction', 'FormSettings' => { + 'Form' => '', 'ParameterMappings' => [2] + })).to be_nil + expect(compiler.send(:sign_out_config, '$Type' => 'Other')).to be_nil + expect(compiler.send(:sign_out_config, '$Type' => 'Forms$SignOutClientAction')) + .to include(type: 'signOut') + expect(compiler.send(:create_object_config, {}, '$Type' => 'Other')).to be_nil + unit.document['Parameters'] << { + 'Name' => 'Item', 'ParameterType' => { '$Type' => 'DataTypes$ObjectType', 'Entity' => 'Demo.Item' } + } + expect(compiler.send(:create_object_config, { 'Name' => 'create' }, { + '$Type' => 'Forms$CreateObjectClientAction', 'EntityRef' => { 'Entity' => 'Demo.Item' }, + 'PageSettings' => { 'Form' => 'Demo.Home' } + })).to include(type: 'createObject') + expect(compiler.send(:open_link_config, '$Type' => 'Forms$OpenLinkClientAction', + 'Address' => { 'Value' => 'https://example.invalid' })) + .to include(type: 'openLink') + expect(compiler.send(:action_handler, '$Type' => 'Forms$FormAction', + 'FormSettings' => { 'Form' => 'Demo.Home' })).to include('openForm2') + expect(compiler.send(:action_handler, '$Type' => 'Forms$UnknownAction')).to be_nil + expect(compiler.send(:open_form_handler, '')).to be_nil + + expect(compiler.send(:entity_ref_destination, nil)).to eq('') + expect(compiler.send(:entity_ref_destination, 'Entity' => 'Demo.Item')).to eq('Demo.Item') + expect(compiler.send(:entity_ref_destination, 'Steps' => [2, { + 'DestinationEntity' => 'Demo.Child' + }])).to eq('Demo.Child') + expect(compiler.send(:flow_return_entity, '$Type' => 'Unknown')).to eq('') + expect(compiler.send(:flow_return_entity, '$Type' => 'Forms$MicroflowSource', + 'MicroflowSettings' => { 'Microflow' => 'Demo.ServerAction' })) + .to eq('') + expect(compiler.send(:positive_integer, 5, 20)).to eq(5) + expect(compiler.send(:positive_integer, 0, 20)).to eq(20) + expect(compiler.send(:positive_integer, 'bad', 20)).to eq(20) + + xpath = instance_double(Mxrb::Compiler::WebListDataSource, xpath?: true, entity: 'Demo.Item') + expect(compiler.send(:list_view_property, { 'Name' => 'items' }, 'key', xpath)) + .to include('DatabaseObjectListProperty') + association = instance_double( + Mxrb::Compiler::WebListDataSource, + xpath?: false, association?: true, association_path: 'Demo.Item_Parent/Demo.Parent' + ) + expect(compiler.send(:list_view_property, { 'Name' => 'items' }, 'key', association)) + .to include('AssociationObjectListProperty') + microflow = instance_double( + Mxrb::Compiler::WebListDataSource, xpath?: false, association?: false, microflow?: true + ) + allow(compiler).to receive(:microflow_argument_map).and_return({}) + expect(compiler.send(:list_view_property, { 'Name' => 'items', 'DataSource' => {} }, 'key', microflow)) + .to include('MicroflowObjectListProperty') + nanoflow = instance_double( + Mxrb::Compiler::WebListDataSource, + xpath?: false, association?: false, microflow?: false, nanoflow_name: 'Demo.ClientAction' + ) + allow(compiler).to receive(:nanoflow_reference).and_return('() => flow') + expect(compiler.send(:list_view_property, { 'Name' => 'items' }, 'key', nanoflow)) + .to include('NanoflowObjectListProperty') + + allow(Mxrb::Compiler::WebListDataSource).to receive(:new).and_return( + instance_double( + Mxrb::Compiler::WebListDataSource, + supported?: true, entity: 'Demo.Item', xpath?: true + ) + ) + expect(compiler.send(:render_widget, { + '$Type' => 'Forms$ListView', 'Name' => 'list', 'Widgets' => [], 'PageSize' => 10 + })).to include('$ListView', 'DatabaseObjectListProperty') + + compiler.instance_variable_set(:@snippet_scopes, [{ 'Context' => { scope: 'snippet', entity: 'Demo.Item' } }]) + expect(compiler.send(:data_source_scope, 'SourceVariable' => { 'SnippetParameter' => 'Context' })) + .to eq(scope: 'snippet', entity: 'Demo.Item') + expect(compiler.send(:data_source_scope, 'SourceVariable' => { 'SnippetParameter' => 'Missing' })).to be_nil + expect(compiler.send(:microflow_object_property, { + 'MicroflowSettings' => { 'Microflow' => 'Demo.ServerAction' } + }, { 'Name' => 'view' }, 'scope')).to include('MicroflowObjectProperty') + + compiler.instance_variable_set(:@list_scopes, [{ scope: 'scope', entity: 'Demo.Item' }]) + expect(compiler.send(:snippet_scope_map, 'Parameters' => [2, { + 'Name' => 'Context', 'ParameterType' => { '$Type' => 'DataTypes$ObjectType' } + }, { 'Name' => 'Ignored', 'ParameterType' => { '$Type' => 'DataTypes$StringType' } }])) + .to include('Context' => include(scope: 'scope')) + expect(compiler.send(:data_view_object_property, { + 'Name' => 'nested', 'DataSource' => { + 'SourceVariable' => { 'PageParameter' => 'Item' }, 'EntityRef' => { 'Steps' => [2, { + 'Association' => 'Demo.Item_Child', 'DestinationEntity' => 'Demo.Child' + }] } + } + }, 'nested-scope')).to include('AssociationObjectProperty') + + unit.document['Parameters'] = [2, { 'Name' => 'PageItem' }] + compiler.instance_variable_set(:@list_scopes, [{ scope: 'list-scope', entity: 'Demo.Item' }]) + expect(compiler.send(:nanoflow_argument, 'Parameter' => 'Flow.Input', 'Expression' => '$PageItem').last) + .to include(kind: 'object') + expect(compiler.send(:nanoflow_argument, 'Parameter' => 'Flow.Input', 'Expression' => '$Item').last + .dig(:expression, :args, :Item, :widget)).to eq('list-scope') + expect(compiler.send(:nanoflow_argument, 'Parameter' => 'Flow.Input', 'Expression' => '$Other').last + .dig(:expression, :args, :Other, :widget)).to eq('$Other') + + flags = %i[ + @uses_form_widgets @uses_date_picker @uses_text_area @uses_radio_button_group + @uses_group_box @uses_file_manager @uses_microflow_object @uses_container + @uses_list_view @uses_nanoflow_object + ] + flags.each { compiler.instance_variable_set(_1, true) } + compiler.instance_variable_set(:@generic_widgets, { 'Custom' => 'example/Custom' }) + imports = compiler.send(:widget_imports) + expect(imports).to include( + 'RadioButtonGroup', 'GroupBox', 'FileManager', 'MicroflowObjectProperty', + 'ListView', 'NanoflowObjectProperty', 'CustomWidgetModule' + ) + end + + it 'executes custom image and generic action-property callbacks' do + source = Mxrb::Compiler::SourceModel.read(@mpr) + compiler = described_class.new(source) + compiler.send(:prepare_compile, source.units_of('Forms$Page').first, 'p') + allow(Mxrb::Compiler::DataGridBundleCompiler).to receive(:new) + .and_return(instance_double(Mxrb::Compiler::DataGridBundleCompiler, supported?: false)) + allow(Mxrb::Compiler::GalleryBundleCompiler).to receive(:new) + .and_return(instance_double(Mxrb::Compiler::GalleryBundleCompiler, supported?: false)) + allow(Mxrb::Compiler::ComboBoxBundleCompiler).to receive(:new) + .and_return(instance_double(Mxrb::Compiler::ComboBoxBundleCompiler, supported?: false)) + + allow(Mxrb::Compiler::ImageBundleCompiler).to receive(:new) do |*, action_property:, **| + rendered = [ + action_property.call('$Type' => 'Forms$SignOutClientAction'), + action_property.call('$Type' => 'Forms$NoAction') + ].compact.join + instance_double(Mxrb::Compiler::ImageBundleCompiler, supported?: true, render: rendered) + end + expect(compiler.send(:render_custom_widget, 'Name' => 'image')).to include('ActionProperty') + + allow(Mxrb::Compiler::ImageBundleCompiler).to receive(:new) + .and_return(instance_double(Mxrb::Compiler::ImageBundleCompiler, supported?: false)) + allow(Mxrb::Compiler::GenericWidgetBundleCompiler).to receive(:new) do |*, render_widgets:, action_property:, **| + rendered = render_widgets.call([{ '$Type' => 'Forms$DynamicText', 'Name' => 'child' }]) + actions = [ + action_property.call('$Type' => 'Forms$CallNanoflowClientAction', + 'Nanoflow' => 'Demo.ClientAction'), + action_property.call('$Type' => 'Forms$SignOutClientAction'), + action_property.call('$Type' => 'Forms$UnknownAction') + ].compact.join + instance_double(Mxrb::Compiler::GenericWidgetBundleCompiler, + supported?: true, component_name: 'Custom', module_path: 'example/Custom', + render: "#{rendered}#{actions}") + end + allow(compiler).to receive(:nanoflow_reference).and_return('() => flow') + expect(compiler.send(:render_custom_widget, 'Name' => 'generic')) + .to include('child', 'callNanoflow', 'signOut') + end + + it 'unwinds snippet scope stacks when child rendering raises' do + source = Mxrb::Compiler::SourceModel.read(@mpr) + compiler = described_class.new(source) + compiler.send(:prepare_compile, source.units_of('Forms$Page').first, 'p') + snippet = Struct.new(:document).new({ 'Widgets' => [2, {}], 'Parameters' => [2] }) + compiler.instance_variable_set(:@snippet_index, { 'Demo.Broken' => snippet }) + allow(compiler).to receive(:children).and_raise(Mxrb::CompilationError, 'broken child') + + expect do + compiler.send(:render_snippet_call, 'Name' => 'call', 'FormCall' => { 'Form' => 'Demo.Broken' }) + end.to raise_error(Mxrb::CompilationError, 'broken child') + expect(compiler.instance_variable_get(:@snippet_scopes)).to be_empty + expect(compiler.instance_variable_get(:@snippet_stack)).to be_empty + end + + it 'covers fail-closed branches for menus, containers, actions, sources, and fields' do # rubocop:disable Metrics/BlockLength + source = Mxrb::Compiler::SourceModel.read(@mpr) + allow(source).to receive(:units_of).and_call_original + allow(source).to receive(:documents).and_call_original + unit = source.units_of('Forms$Page').first + compiler = described_class.new(source) + compiler.send(:prepare_compile, unit, 'p') + + expect(compiler.send(:render_menu, { 'Name' => 'menu', 'MenuSource' => nil }, 'MenuBar')) + .to include('mxrb-unsupported-widget') + expect(compiler.send(:menu_items, '$Type' => 'Forms$MenuDocumentSource', + 'Menu' => 'Demo.Missing')).to be_nil + expect(compiler.send(:menu_items, '$Type' => 'Forms$NavigationSource', + 'NavigationProfile' => 'Missing')).to be_nil + expect(compiler.send(:render_snippet_call, 'Name' => 'missing', + 'FormCall' => { 'Form' => 'Demo.Missing' })) + .to include('mxrb-unsupported-widget') + expect(compiler.send(:menu_microflow_config, { + '$Type' => 'Forms$MicroflowAction', 'MicroflowSettings' => { + 'Microflow' => 'Demo.ServerAction', 'ParameterMappings' => [2, { + 'Parameter' => 'Demo.ServerAction.Item', 'Expression' => '$Item' + }] + } + })).to be_nil + + parameterized = { + '$Type' => 'Forms$DivContainer', 'Name' => 'clickable', 'Widgets' => [], + 'OnClickAction' => { '$Type' => 'Forms$FormAction', 'FormSettings' => { + 'Form' => 'Demo.Home', 'ParameterMappings' => [2, {}] + } } + } + expect(compiler.send(:render_container, parameterized)).to include('role') + expect(compiler.send(:render_container, { + '$Type' => 'Forms$DivContainer', 'Name' => 'unknown', 'Widgets' => [], + 'OnClickAction' => { '$Type' => 'Forms$UnknownAction' } + })).not_to include('role') + expect(compiler.send(:render_action_button, { + '$Type' => 'Forms$ActionButton', 'Name' => 'legacy', 'Action' => parameterized['OnClickAction'] + })).not_to include('"disabled": true') + expect(compiler.send(:open_page_config, parameterized['OnClickAction'])).to be_nil + expect(compiler.send(:action_handler, {})).to be_nil + + expect(compiler.send(:create_object_config, {}, { + '$Type' => 'Forms$CreateObjectClientAction', 'EntityRef' => { 'Entity' => 'Missing.Entity' }, + 'PageSettings' => { 'Form' => 'Demo.Missing' } + })).to be_nil + missing_layout_page = Struct.new(:document).new({ 'FormCall' => { 'Form' => 'Demo.MissingLayout' } }) + expect(compiler.send(:page_location, missing_layout_page)).to eq('content') + expect(compiler.send(:allowed_roles_for, 'Forms$Page', 'Demo.Missing')).to eq([]) + popup_layout = Struct.new(:module_name, :document).new('Demo', { + 'Name' => 'Popup', 'CanvasWidth' => 600 + }) + allow(source).to receive(:units_of).with('Forms$Layout').and_return([popup_layout]) + popup_page = Struct.new(:document).new({ 'FormCall' => { 'Form' => 'Demo.Popup' } }) + expect(compiler.send(:page_location, popup_page)).to eq('modal') + allow(source).to receive(:documents).with('Security$ProjectSecurity').and_return([{ + 'UserRoles' => [2, { 'Name' => 'Admin', 'ModuleRoles' => [1, 'Demo.Admin'] }] + }]) + role_unit = Struct.new(:module_name, :document).new('Demo', { + 'Name' => 'Secure', 'AllowedModuleRoles' => [1, 'Demo.Admin'] + }) + allow(source).to receive(:units_of).with('Forms$Page').and_return([role_unit]) + expect(compiler.send(:allowed_roles_for, 'Forms$Page', 'Demo.Secure')).to eq(['Admin']) + allow(source).to receive(:documents).with('Security$ProjectSecurity').and_return([]) + expect(compiler.send(:allowed_roles_for, 'Forms$Page', 'Demo.Secure')).to eq([]) + + invalid_mapping = { 'Parameter' => '', 'Expression' => 'invalid' } + expect(compiler.send(:nanoflow_argument_map, 'ParameterMappings' => [2, invalid_mapping])).to be_nil + expect(compiler.send(:nanoflow_argument, invalid_mapping)).to be_nil + compiler.instance_variable_set(:@list_scopes, [{ scope: 'scope', entity: 'Demo.Item' }]) + expect(compiler.send(:data_view_object_property, { + 'Name' => 'child', 'DataSource' => { 'EntityRef' => { 'Steps' => [2, { + 'Association' => 'Demo.Item_Child', 'DestinationEntity' => 'Demo.Child' + }] } } + }, 'child')).to include('AssociationObjectProperty') + compiler.instance_variable_set(:@list_scopes, []) + expect(compiler.send(:data_view_object_property, { + 'Name' => 'flow', 'DataSource' => { + '$Type' => 'Forms$MicroflowSource', 'MicroflowSettings' => { 'Microflow' => 'Demo.ServerAction' } + } + }, 'flow')).to include('MicroflowObjectProperty') + expect(compiler.send(:association_object_property, 'scope', '', 'child')) + .not_to include('operationId') + expect(compiler.send(:microflow_object_property, { + 'MicroflowSettings' => { 'Microflow' => '' } + }, { 'Name' => 'child' }, 'scope')).to be_nil + expect(compiler.send(:flow_return_entity, '$Type' => 'Forms$MicroflowSource', + 'MicroflowSettings' => { 'Microflow' => 'Demo.Missing' })).to eq('') + + unsupported_source = instance_double(Mxrb::Compiler::WebListDataSource, supported?: false, entity: '') + allow(Mxrb::Compiler::WebListDataSource).to receive(:new).and_return(unsupported_source) + expect(compiler.send(:render_list_view, '$Type' => 'Forms$ListView', 'Name' => 'bad')) + .to include('mxrb-unsupported-widget') + association = instance_double( + Mxrb::Compiler::WebListDataSource, + xpath?: false, association?: true, association_path: 'Demo.Item_Parent/Demo.Parent' + ) + compiler.instance_variable_set(:@list_scopes, []) + compiler.instance_variable_set(:@data_view_scopes, []) + expect(compiler.send(:list_view_property, { 'Name' => 'items' }, 'key', association)).to be_nil + microflow = instance_double( + Mxrb::Compiler::WebListDataSource, xpath?: false, association?: false, microflow?: true + ) + allow(compiler).to receive(:microflow_argument_map).and_return(nil) + expect(compiler.send(:list_view_property, { 'Name' => 'items' }, 'key', microflow)).to be_nil + nanoflow = instance_double( + Mxrb::Compiler::WebListDataSource, + xpath?: false, association?: false, microflow?: false, nanoflow_name: 'Demo.Missing' + ) + allow(compiler).to receive(:nanoflow_reference).and_return(nil) + expect(compiler.send(:list_view_property, { 'Name' => 'items' }, 'key', nanoflow)).to be_nil + supported_without_value = instance_double( + Mxrb::Compiler::WebListDataSource, + supported?: true, entity: 'Demo.Item', xpath?: false, association?: false, + microflow?: false, nanoflow_name: 'Demo.Missing' + ) + allow(Mxrb::Compiler::WebListDataSource).to receive(:new).and_return(supported_without_value) + allow(compiler).to receive(:nanoflow_reference).and_return(nil) + expect(compiler.send(:render_list_view, '$Type' => 'Forms$ListView', 'Name' => 'missingFlow')) + .to include('mxrb-unsupported-widget') + + invalid_field_renderers = %i[ + render_text_area render_radio_button_group render_file_manager render_date_picker render_check_box + ] + invalid_field_renderers.each do |method| + expect(compiler.send(method, { '$Type' => 'Forms$Unknown', 'Name' => method.to_s })) + .to include('mxrb-unsupported-widget') + end + compiler.instance_variable_set(:@data_view_scopes, [{ scope: 'scope', entity: 'Demo.Item' }]) + expect(compiler.send(:render_file_manager, { 'Name' => 'download', 'Type' => 'Download' })) + .to include('"widgetType": "download"') + + expect(compiler.send(:custom_widget_identifier, 'Object' => {})).to eq('CustomWidgets$CustomWidget') + allow(source).to receive(:document_index).and_return({ + 'schema' => { 'ObjectType' => { '$ID' => 'other' } } + }) + expect(compiler.send(:custom_widget_identifier, 'Object' => { 'TypePointer' => 'missing' })) + .to eq('CustomWidgets$CustomWidget') + allow(source).to receive(:document_index).and_return({ + 'schema' => { 'ObjectType' => { '$ID' => 'object' }, 'WidgetId' => 'Vendor.Widget' } + }) + expect(compiler.send(:custom_widget_identifier, 'Object' => { 'TypePointer' => 'object' })) + .to eq('Vendor.Widget') + allow(source).to receive(:document_index).and_return({ + 'schema' => { 'ObjectType' => { '$ID' => 'object' }, 'WidgetId' => '' } + }) + expect(compiler.send(:custom_widget_identifier, 'Object' => { 'TypePointer' => 'object' })) + .to eq('CustomWidgets$CustomWidget') + compiler.instance_variable_set(:@generic_widgets, nil) + %i[ + @uses_data_grid @uses_form_widgets @uses_gallery @uses_bound_text @uses_tab_container + @uses_image @uses_conditional @uses_dynamic_class @uses_list_view + ].each { compiler.instance_variable_set(_1, false) } + compiler.instance_variable_set(:@uses_list_view, false) + compiler.instance_variable_set(:@uses_layout_widgets, true) + expect(compiler.send(:widget_imports)).to include('ScrollContainer') + compiler.instance_variable_set(:@layout_mode, true) + compiler.instance_variable_set(:@nanoflow_programs, double(declarations: '')) + allow(compiler).to receive(:parent_layout_name).and_return('') + expect(compiler.send(:layout_module_source, '[]')).to include('Object.assign({}, {}') + allow(compiler).to receive(:parent_layout_name).and_return('Demo.Shell') + expect(compiler.send(:layout_module_source, '[]')).to include('Object.assign({}, parentContent') + allow(compiler).to receive(:parent_layout_name).and_call_original + compiler.instance_variable_set(:@layout_mode, false) + compiler.instance_variable_set(:@unit, unit) + shell = Struct.new(:module_name, :document).new('Demo', { 'Name' => 'Shell' }) + allow(source).to receive(:units_of).with('Forms$Layout').and_return([shell]) + allow(source).to receive(:web_layout?).and_return(false) + expect(compiler.send(:parent_layout_name)).to eq('') + expect(compiler.send(:action_handler, '$Type' => '')).to be_nil + end end RSpec.describe Mxrb::Compiler::ImageBundleCompiler do @@ -759,6 +1147,50 @@ def image_compiler(datasource: 'image', image: 'Demo.Assets.Logo', widget_id: de expect(missing).not_to be_supported expect(missing.send(:image_uri)).to be_nil end + + it 'compiles dynamic image URLs and optional actions while failing closed for malformed values' do + compiler = image_compiler + action = { '$Type' => 'Forms$MicroflowAction' } + compiler.instance_variable_get(:@values)['onClick'] = ['Action', { 'Action' => action }] + compiler.instance_variable_set(:@action_property, ->(value) { "ActionProperty(#{value['$Type']})" }) + expect(compiler).to be_supported + expect(compiler.render).to include('ActionProperty(Forms$MicroflowAction)') + + compiler.instance_variable_set(:@action_property, nil) + expect(compiler).not_to be_supported + expect(compiler.send(:compiled_action)).to be_nil + compiler.instance_variable_get(:@values)['onClick'] = ['Action', { + 'Action' => { '$Type' => 'Forms$NoAction' } + }] + expect(compiler.send(:compiled_action)).to be_nil + + compiler.instance_variable_get(:@values).delete('onClick') + expect(compiler.send(:supported_action?)).to be(true) + expect(compiler.send(:image_url_parameter)).to be_nil + compiler.instance_variable_get(:@values).delete('imageUrl') + expect(compiler.send(:image_url_parameter)).to be_nil + compiler.instance_variable_get(:@values)['imageUrl'] = [] + expect(compiler.send(:image_url_parameter)).to be_nil + + compiler.instance_variable_set(:@scope, 'p.Demo.Home.context') + compiler.instance_variable_get(:@values)['datasource'] = ['Enumeration', { 'PrimitiveValue' => 'imageUrl' }] + compiler.instance_variable_get(:@values)['imageUrl'] = ['TextTemplate', { + 'TextTemplate' => { 'Parameters' => [2, { + 'AttributeRef' => { + 'Attribute' => 'Demo.Item.Picture', 'EntityRef' => { 'Steps' => [2, { + 'Association' => 'Demo.Parent_Item', 'DestinationEntity' => 'Demo.Item' + }] } + } + }] } + }] + expect(compiler).to be_supported + expect(compiler.render).to include('Demo.Parent_Item/Demo.Item/Picture') + + compiler.instance_variable_set(:@scope, nil) + expect(compiler.send(:dynamic_image_url)).to be_nil + compiler.instance_variable_get(:@values)['imageUrl'] = ['TextTemplate', { 'TextTemplate' => nil }] + expect(compiler.send(:image_url_parameter)).to be_nil + end end RSpec.describe Mxrb::Compiler::PageBundleBuilder do @@ -816,5 +1248,24 @@ def image_compiler(datasource: 'image', image: 'Demo.Assets.Logo', widget_id: de .to raise_error(Mxrb::CompilationError, /invalid widget package/) end end + + it 'extracts root-level JavaScript, CSS, and assets beside a root runtime module' do + Dir.mktmpdir do |root| + widgets = File.join(root, 'widgets') + web = File.join(root, 'web') + FileUtils.mkdir_p(widgets) + Zip::File.open(File.join(widgets, 'root.mpk'), create: true) do |zip| + zip.get_output_stream('Widget.mjs') { _1.write('export default {};') } + zip.get_output_stream('Widget.css') { _1.write('.widget {}') } + zip.get_output_stream('assets/icon.svg') { _1.write('') } + zip.get_output_stream('ignored.xml') { _1.write('') } + end + + expect(described_class.new(root, web).extract).to eq(3) + expect(File).to exist(File.join(web, 'widgets', 'Widget.css')) + expect(File).to exist(File.join(web, 'widgets', 'assets', 'icon.svg')) + expect(File).not_to exist(File.join(web, 'widgets', 'ignored.xml')) + end + end end # rubocop:enable Metrics/BlockLength diff --git a/spec/page_container_renderer_spec.rb b/spec/page_container_renderer_spec.rb new file mode 100644 index 0000000..fb914ae --- /dev/null +++ b/spec/page_container_renderer_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'tmpdir' + +# rubocop:disable Metrics/BlockLength, Metrics/MethodLength +RSpec.describe 'native page container rendering' do + around do |example| + Dir.mktmpdir do |root| + @mpr = File.join(root, 'Containers.mpr') + Mxrb.define(@mpr) do + mendix_version '11.12.1' + self.module(:Demo) do + entity(:Item) { string :Name } + page :Home + end + end + @source = Mxrb::Compiler::SourceModel.read(@mpr) + @unit = @source.units_of('Forms$Page').first + example.run + end + end + + def page_widgets=(widgets) + argument = @unit.document.dig('FormCall', 'Arguments').find { _1.is_a?(Hash) } + argument['Widgets'] = [2, *widgets] + end + + def table + { + '$Type' => 'Forms$Table', 'Name' => 'details', 'Appearance' => { 'Class' => 'details' }, + 'Rows' => [2, { '$Type' => 'Forms$TableRow', 'Appearance' => { 'Class' => 'record' } }], + 'Cells' => [2, + { + '$Type' => 'Forms$DbTableCell', 'Name' => 'header', 'IsHeader' => true, + 'TopRowIndex' => 0, 'LeftColumnIndex' => 0, 'Width' => 2, 'Height' => 1, + 'Appearance' => { 'Class' => 'heading' }, 'Widgets' => [2] + }, + { + '$Type' => 'Forms$DbTableCell', 'Name' => 'value', 'IsHeader' => false, + 'TopRowIndex' => 0, 'LeftColumnIndex' => 2, 'Width' => 'bad', 'Height' => nil, + 'Widgets' => [2, { + '$Type' => 'Forms$DynamicText', 'Name' => 'name', 'RenderMode' => 'Text', + 'Content' => { 'Template' => { 'Items' => [2, { 'Text' => 'Name' }] } } + }] + }] + } + end + + def list_view + { + '$Type' => 'Forms$ListView', 'Name' => 'items', 'PageSize' => 10, + 'DataSource' => { + '$Type' => 'Forms$ListViewXPathSource', + 'EntityRef' => { 'Entity' => 'Demo.Item' }, 'XPathConstraint' => '' + }, + 'Widgets' => [2, { + '$Type' => 'Forms$DynamicText', 'Name' => 'itemName', 'RenderMode' => 'Text', + 'Content' => { + 'Template' => { 'Items' => [2, { 'Text' => '{1}' }] }, + 'Parameters' => [2, { 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name' } }] + } + }] + } + end + + it 'renders tables, XPath list views, and a DataView listening to list selection' do + listener = { + '$Type' => 'Forms$DataView', 'Name' => 'selected', 'ShowFooter' => false, + 'DataSource' => { '$Type' => 'Forms$ListenTargetSource', 'ListenTarget' => 'items' }, + 'NoEntityMessage' => { 'Items' => [2] }, 'Widgets' => [2, table], 'FooterWidgets' => [2] + } + self.page_widgets = [list_view, listener] + + bundle = Mxrb::Compiler::PageBundleCompiler.new(@source).compile(@unit) + + expect(bundle).to have_attributes(unsupported_widgets: [], unsupported_custom_widgets: []) + expect(bundle.source).to include( + 'React.createElement("table"', 'React.createElement("tbody"', 'React.createElement("tr"', + 'React.createElement("th"', 'React.createElement("td"', '"colSpan": 2', '"colSpan": 1', + '$ListView', 'ListenObjectProperty', + '"listenTo": "p.Demo.Home.items"', '"entity": "Demo.Item"' + ) + end + + it 'fails closed for invalid listeners and resolves every structured microflow variable scope' do + self.page_widgets = [list_view] + compiler = Mxrb::Compiler::PageBundleCompiler.new(@source) + compiler.send(:prepare_compile, @unit, 'p') + compiler.instance_variable_set( + :@snippet_scopes, [{ 'SnippetItem' => { scope: 'p.Demo.Home.items', entity: 'Demo.Item' } }] + ) + + expect(compiler.send(:listen_object_property, { 'ListenTarget' => 'missing' }, { 'Name' => 'view' })) + .to be_nil + expect(compiler.send(:data_view_entity, { + 'DataSource' => { '$Type' => 'Forms$ListenTargetSource', 'ListenTarget' => 'missing' } + })).to eq('') + expect(compiler.send(:integer_or, nil, 7)).to eq(7) + expect(compiler.send(:all_page_widgets, 'scalar')).to eq([]) + + scopes = { + { 'SnippetParameter' => 'SnippetItem' } => 'p.Demo.Home.items', + { 'PageParameter' => 'PageItem' } => '$PageItem', + { 'Widget' => 'items' } => 'p.Demo.Home.items', + { 'LocalVariable' => 'LocalItem' } => '$LocalItem' + } + scopes.each do |variable, expected| + expect(compiler.send(:microflow_variable_scope, variable)).to eq(expected) + end + expect(compiler.send(:microflow_argument, { + 'Parameter' => 'Demo.Action.Item', 'Expression' => '', + 'Variable' => { 'SnippetParameter' => 'SnippetItem' } + })).to eq([:Item, { widget: 'p.Demo.Home.items', source: 'object' }]) + expect(compiler.send(:microflow_variable_scope, nil)).to be_nil + expect(compiler.send(:microflow_variable_scope, 'SnippetParameter' => 'Missing')).to be_nil + end +end +# rubocop:enable Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/protocols_spec.rb b/spec/protocols_spec.rb index 98208e1..0beb2f1 100644 --- a/spec/protocols_spec.rb +++ b/spec/protocols_spec.rb @@ -8,7 +8,7 @@ def entry(protocol:, guids: [], name: 'Test', marketplace_id: '1') described_class::Entry.new( protocol: protocol, name: name, publisher: 'Mendix', category: 'Messaging', - marketplace_id: marketplace_id, appstore_guids: guids.freeze, + marketplace_id: marketplace_id, content_type: 'Module', appstore_guids: guids.freeze, source_url: "https://marketplace.mendix.com/link/component/#{marketplace_id}", evidence_date: '2026-08-02' ) @@ -25,7 +25,9 @@ def project(*modules) = instance_double(Mxrb::Model::Project, modules: modules) it 'exposes an immutable registry of connectors with verifiable provenance' do expect(described_class.all).to be_frozen expect(described_class.all).to all(be_a(described_class::Entry)) - expect(described_class.all.map(&:marketplace_id)).to contain_exactly('119508', '230843', '117391') + expect(described_class.all.map(&:marketplace_id)).to contain_exactly( + '119508', '230843', '117391', '105878', '235426', '118800' + ) expect(described_class.all.map(&:source_url)).to all(start_with('https://marketplace.mendix.com/')) end @@ -33,9 +35,72 @@ def project(*modules) = instance_double(Mxrb::Model::Project, modules: modules) expect(described_class.find_by_protocol(:opc_ua).map(&:name)) .to contain_exactly('OPC-UA Connector', 'OPC UA Client Connector') expect(described_class.find_by_protocol('MQTT').map(&:name)).to eq(['MQTT']) + expect(described_class.find_by_protocol(:kafka).map(&:name)).to eq(['Kafka']) + expect(described_class.find_by_protocol('websocket').map(&:name)).to eq(['WebsocketClient']) + expect(described_class.find_by_protocol(:amqp).map(&:name)).to eq(['eMagiz Mendix Connector (Legacy)']) expect(described_class.find_by_protocol(:modbus)).to be_empty end + it 'previews connector installation without writing and applies only through explicit adapters' do + package = Mxrb::OfficialMarketplace::OfficialPackage.new( + 'Kafka', '2.12.0', :mendix, 'https://example.test/kafka.mpk', 'content:105878', + 105_878, '123e4567-e89b-12d3-a456-426614174000', 'Module', 'Regular', [], false, true + ) + api = instance_double(Mxrb::OfficialMarketplace::ContentApi) + installer = instance_double(Mxrb::OfficialMarketplace::Installer) + allow(api).to receive(:resolve).with( + '105878', version: '2.12.0', mendix_version: '10.24.0' + ).and_return(package) + allow(installer).to receive(:pull_official).with( + '105878', version: '2.12.0', mendix_version: '10.24.0', api: + ).and_return(:installed) + + adapter = described_class.adapter(installer:, api:) + plan = described_class.plan(:kafka, mendix_version: '10.24.0', version: '2.12.0', adapter:) + expect(plan).to be_safe + expect(plan.changes.first).to include( + action: :install, protocol: :kafka, marketplace_id: '105878', version: '2.12.0' + ) + expect(plan.apply!).to eq(:installed) + expect(plan).to be_applied + expect { plan.apply! }.to raise_error(Mxrb::MarketplaceError, /already applied/) + end + + it 'fails closed for unverified protocols and adapter-free apply' do + expect { described_class.plan(:modbus, mendix_version: '10.24.0') } + .to raise_error(Mxrb::MarketplaceError, /no verified Marketplace connector/) + + plan = described_class.plan(:websocket, mendix_version: '10.24.0', version: '1.0.0') + expect(plan).not_to be_safe + expect(plan.changes.first[:version]).to eq('1.0.0') + expect(plan.blocked_reasons).to include(/adapter was not provided/, /version was not resolved/) + expect { plan.apply! }.to raise_error(Mxrb::MarketplaceError, /blocked/) + + adapter = described_class.adapter(installer: double, api: nil) + unresolved = described_class.plan(:kafka, mendix_version: '10.24.0', adapter:) + expect(unresolved.blocked_reasons).to eq(['official Marketplace version was not resolved']) + + selected = described_class.plan( + :opc_ua, mendix_version: '10.24.0', marketplace_id: 117_391 + ) + expect(selected.entry.name).to eq('OPC UA Client Connector') + expect { described_class.plan(:opc_ua, mendix_version: '10.24.0', marketplace_id: 'missing') } + .to raise_error(Mxrb::MarketplaceError, /no verified Marketplace connector/) + end + + it 'exposes connector declarations in the Ruby builder without emitting an incomplete MPR' do + builder = Mxrb::Dsl::Builder.new('/tmp/connector-plan.mpr') + builder.mendix_version('10.24.0') + builder.connector(:opc_ua, version: '2.2.0') + + expect(builder.definition[:connectors]).to eq( + [{ protocol: :opc_ua, version: '2.2.0', marketplace_id: nil }] + ) + expect(builder.connector_plans.first.entry.marketplace_id).to eq('230843') + expect { builder.build! }.to raise_error(Mxrb::MarketplaceError, /preview-only/) + expect(File).not_to exist('/tmp/connector-plan.mpr') + end + it 'identifies connectors only by verified GUIDs and fails closed otherwise' do verified = entry(protocol: :mqtt, guids: ['real-guid']) expect(verified.matches_guid?('real-guid')).to be(true) diff --git a/spec/scaffold_spec.rb b/spec/scaffold_spec.rb index 990a4d1..2ed5511 100644 --- a/spec/scaffold_spec.rb +++ b/spec/scaffold_spec.rb @@ -72,7 +72,7 @@ def scaffold(root, kind, name = nil) load File.join(root, 'project.rb') mpr_file = Mxrb::IO::MprFile.open(File.join(root, 'ScaffoldApp.mpr'), readonly: true) security = mpr_file.all_units.map { mpr_file.parse_contents(_1) } - .find { _1['$Type'] == 'Security$ProjectSecurity' } + .find { _1['$Type'] == 'Security$ProjectSecurity' } expect(security['AdminUserRole']).to eq('Administrator') mpr_file.close Mxrb.open(File.join(root, 'ScaffoldApp.mpr')) do |project| diff --git a/spec/security_materializer_spec.rb b/spec/security_materializer_spec.rb index 7dffbbe..4cf64da 100644 --- a/spec/security_materializer_spec.rb +++ b/spec/security_materializer_spec.rb @@ -24,7 +24,7 @@ end mpr = Mxrb::IO::MprFile.open(@mpr, readonly: true) @source = mpr.all_units.map { mpr.parse_contents(_1) } - .find { _1['$Type'] == 'Security$ProjectSecurity' } + .find { _1['$Type'] == 'Security$ProjectSecurity' } mpr.close FileUtils.mkdir_p(File.join(@deployment, 'model')) other = { '$ID' => '11111111-1111-4111-8111-111111111111', '$Type' => 'Projects$Project' } diff --git a/spec/semantic_search_spec.rb b/spec/semantic_search_spec.rb index 3242467..2efb8ea 100644 --- a/spec/semantic_search_spec.rb +++ b/spec/semantic_search_spec.rb @@ -128,6 +128,9 @@ def make_mpr(path) stub_const('Informers', class_double('Informers', pipeline:)) embedder = described_class.allocate allow(embedder).to receive(:require).with('informers').and_return(true) + expect(Informers).to receive(:pipeline).with( + 'embedding', described_class::MODEL, revision: described_class::MODEL_REVISION + ).and_return(pipeline) embedder.send(:initialize) expect(embedder.embed('order')).to eq([0.1, 0.9]) diff --git a/spec/web_list_data_source_spec.rb b/spec/web_list_data_source_spec.rb index f6c5594..f4a9d94 100644 --- a/spec/web_list_data_source_spec.rb +++ b/spec/web_list_data_source_spec.rb @@ -105,5 +105,56 @@ def source(items) expect(resolver.send(:connection_unconfigured?, connection.document.merge('ConnectionString' => ''))) .to be(false) end + + it 'resolves direct database and association sources, including entity fallbacks' do + database = { + '$Type' => 'Forms$DatabaseSource', + 'EntityRef' => { 'Entity' => 'Demo.Direct' }, + 'XPathConstraint' => '[Active = true]' + } + direct = described_class.new(source([]), 'DataSource' => database) + expect(direct).to be_xpath + expect(direct.entity).to eq('Demo.Direct') + expect(direct.xpath_constraint).to eq('[Active = true]') + + association = { + '$Type' => 'Forms$AssociationSource', + 'EntityRef' => { + 'Steps' => [2, { 'Association' => 'Demo.Parent_Items', + 'DestinationEntity' => 'Demo.Item' }] + } + } + associated = described_class.new(source([]), 'DataSource' => association) + expect(associated).to be_association + expect(associated.association_path).to eq('Demo.Parent_Items/Demo.Item') + expect(associated.entity).to eq('Demo.Item') + end + + it 'walks nested source arrays and rejects ambiguous connector actions and non-hashes' do + nested_widget = { + 'Children' => [ + 2, + { '$Type' => 'CustomWidgets$CustomWidgetXPathSource', + 'EntityRef' => { 'Entity' => 'Demo.Nested' } } + ] + } + nested = described_class.new(source([]), nested_widget) + expect(nested).to be_xpath + expect(nested.entity).to eq('Demo.Nested') + + empty = described_class.new(source([]), nil) + expect(empty).not_to be_supported + expect(empty.send(:nested, 'scalar', 'Anything')).to eq([]) + + action = { '$Type' => 'DatabaseConnector$ExecuteDatabaseQueryAction' } + flow = unit(module_name: 'Demo', document: { + '$Type' => 'Microflows$Microflow', 'Name' => 'LoadItems', + 'MicroflowReturnType' => { 'Entity' => 'Demo.Item' }, + 'Actions' => [action, action] + }) + ambiguous = described_class.new(source([flow]), widget) + expect(ambiguous).to be_microflow + expect(ambiguous).not_to be_xpath + end end # rubocop:enable Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/web_operation_compiler_spec.rb b/spec/web_operation_compiler_spec.rb index a378fb5..81c6f29 100644 --- a/spec/web_operation_compiler_spec.rb +++ b/spec/web_operation_compiler_spec.rb @@ -253,6 +253,234 @@ def widget(source: true) expect(described_class.new(source).send(:allowed_user_role_sets, page)) .to eq([['Administrator']]) end + + it 'covers defensive menu, data-action, association, and nanoflow operation paths' do # rubocop:disable Metrics/BlockLength + expect(described_class.menu_operation_id('$ID' => SecureRandom.uuid)).to be_a(String) + expect(described_class.menu_operation_id( + 'MicroflowSettings' => { 'Microflow' => 'Demo.Run' } + )).to be_a(String) + + source = instance_double(Mxrb::Compiler::SourceModel, document_index: {}) + allow(source).to receive(:units_of).and_return([]) + allow(source).to receive(:documents).and_return([]) + compiler = described_class.new(source) + + expect(compiler.send(:custom_widget_id, {})).to be_nil + expect(compiler.send(:custom_property_values, nil)).to eq({}) + expect(compiler.send(:custom_property_values, 'Properties' => [2, { 'TypePointer' => 'missing' }])) + .to eq({}) + expect(compiler.send(:grid_filter_specs, {})).to eq([]) + expect(compiler.send(:popup_layout_document?, nil)).to be(false) + expect(compiler.send(:popup_layout_document?, 'Name' => 'PopupLayout')).to be(true) + expect(compiler.send(:popup_layout_document?, 'Name' => 'Shell', 'CanvasWidth' => 600)).to be(true) + expect(compiler.send(:popup_layout_document?, { + 'Name' => 'Shell', 'CanvasWidth' => 900, 'Widget' => { '$Type' => 'Forms$Header' } + })).to be(false) + + expect(compiler.send(:resolved_menu_items, nil)).to eq([]) + expect(compiler.send(:resolved_menu_items, '$Type' => 'Unknown')).to eq([]) + expect(compiler.send(:resolved_menu_items, '$Type' => 'Forms$MenuDocumentSource', + 'Menu' => 'Demo.Missing')).to eq([]) + expect(compiler.send(:resolved_menu_items, '$Type' => 'Forms$NavigationSource', + 'NavigationProfile' => 'Missing')).to eq([]) + expect(compiler.send(:menu_microflow_operation, '$Type' => 'Forms$MicroflowAction', + 'MicroflowSettings' => {})).to be_nil + expect(compiler.send(:page_related_documents, { + 'Widget' => { '$Type' => 'Forms$SnippetCallWidget', 'FormCall' => { 'Form' => '' } } + })).to be_an(Array) + + actions = { + 'Widgets' => [2, + { '$Type' => 'Forms$ActionButton', 'Name' => 'delete', + 'Action' => { '$Type' => 'Forms$DeleteClientAction' } }, + { '$Type' => 'Forms$ActionButton', 'Name' => 'create', + 'Action' => { '$Type' => 'Forms$CreateObjectClientAction', + 'EntityRef' => { 'Entity' => 'Demo.Item' } } }] + } + operations = compiler.send(:data_action_operations, 'Demo.Home', actions, []) + expect(operations.map { _1['operationType'] }).to eq(%w[delete create]) + expect(compiler.send(:microflow_parameters, 'System.ShowHomePage')).to eq({}) + expect(compiler.send(:supported_microflow_parameter?, 'VariableType' => { + '$Type' => 'DataTypes$ObjectType', 'Entity' => '' + })).to be(false) + + data_view = { '$Type' => 'Forms$DataView', 'Name' => 'view', 'DataSource' => { + '$Type' => 'Forms$MicroflowSource', 'MicroflowSettings' => { 'Microflow' => '' } + } } + expect(compiler.send(:data_view_operation, 'Demo.Home', data_view, [])).to be_nil + expect(compiler.send(:association_retrieve_operation, 'Demo.Home', {}, [], 'Demo.Missing/Demo.Item')) + .to be_nil + expect(compiler.send(:call_microflow_operation, 'Demo.Home', {}, [], 'Demo.Missing')).to be_nil + expect(compiler.send(:retrieve_by_microflow_operation, 'Demo.Home', {}, [], 'Demo.Missing')).to be_nil + expect(compiler.send(:entity_ref_path, nil)).to eq('') + expect(compiler.send(:association_source_entity, 'Missing.Association/Demo.Item')).to be_nil + expect(compiler.send(:document_scope_entity, 'Parameters' => [2, { + 'ParameterType' => { 'ObjectType' => { 'Entity' => 'Demo.Legacy' } } + }])).to eq('Demo.Legacy') + + expect(compiler.send(:widget_scope_entity, '$Type' => 'Forms$DataView', 'DataSource' => { + 'EntityRef' => { 'Entity' => 'Demo.Item' } + })).to eq('Demo.Item') + expect(compiler.send(:microflow_return_entity, 'MicroflowSettings' => { + 'Microflow' => 'Demo.Missing' + })).to be_nil + expect(compiler.send(:nanoflow_operations)).to eq([]) + + activity = { '$ID' => SecureRandom.uuid } + expect(compiler.send(:nanoflow_server_operation, 'Demo.Client', activity, { + '$Type' => 'Microflows$MicroflowCallAction', 'MicroflowCall' => {} + }, {})).to be_nil + expect(compiler.send(:nanoflow_server_operation, 'Demo.Client', activity, { + '$Type' => 'Microflows$CommitAction', 'CommitVariableName' => 'Item' + }, 'Item' => 'Demo.Item')).to include( + 'operationType' => 'commit', 'parameters' => { 'Objects' => ['[Demo.Item]'] } + ) + expect(compiler.send(:nanoflow_server_operation, 'Demo.Client', activity, { + '$Type' => 'Microflows$CommitAction' + }, {})).to include('parameters' => { 'Objects' => ['AnyObjectList'] }) + expect(compiler.send(:commit_action?, '$Type' => 'Microflows$ChangeAction', 'Commit' => 'Yes')) + .to be(true) + expect(compiler.send(:commit_action?, '$Type' => 'Other')).to be(false) + + variables = compiler.send(:nanoflow_variable_entities, { + 'Objects' => [2, + { '$Type' => 'Microflows$MicroflowParameter', 'Name' => 'Empty', + 'VariableType' => { 'Entity' => '' } }, + { '$Type' => 'Microflows$ActionActivity', 'Action' => { + 'OutputVariableName' => 'Created', 'Entity' => 'Demo.Item' + } }] + }) + expect(variables).to eq('Created' => 'Demo.Item') + end + + it 'materializes successful menu, microflow, and association operation records' do + parent_id = SecureRandom.uuid + flow = unit(module_name: 'Demo', document: { + '$Type' => 'Microflows$Microflow', 'Name' => 'Load', + 'ObjectCollection' => { 'Objects' => [2] } + }) + domain = unit(module_name: 'Demo', document: { + 'Associations' => [2, { 'Name' => 'Parent_Items', 'ParentPointer' => parent_id }], + 'Entities' => [2, { '$ID' => parent_id, 'Name' => 'Parent' }] + }) + source = instance_double(Mxrb::Compiler::SourceModel, document_index: {}) + allow(source).to receive(:units_of) do |type| + { 'Microflows$Microflow' => [flow], 'DomainModels$DomainModel' => [domain] }.fetch(type, []) + end + allow(source).to receive(:units).and_return([flow, domain]) + allow(source).to receive(:documents).and_return([]) + compiler = described_class.new(source) + widget = { 'Name' => 'items' } + + menu = compiler.send(:menu_microflow_operation, { + '$ID' => SecureRandom.uuid, '$Type' => 'Forms$MicroflowAction', + 'MicroflowSettings' => { 'Microflow' => 'Demo.Load' } + }) + expect(menu).to include('operationType' => 'callMicroflow') + expect(compiler.send(:call_microflow_operation, 'Demo.Home', widget, [], 'Demo.Load')) + .to include('operationType' => 'callMicroflow') + expect(compiler.send(:retrieve_by_microflow_operation, 'Demo.Home', widget, [], 'Demo.Load')) + .to include('operationType' => 'retrieveByMicroflow') + expect(compiler.send(:data_view_operation, 'Demo.Home', { + '$Type' => 'Forms$DataView', 'Name' => 'view', 'DataSource' => { + '$Type' => 'Forms$MicroflowSource', 'MicroflowSettings' => { 'Microflow' => 'Demo.Load' } + } + }, [])).to include('operationType' => 'retrieveByMicroflow') + path = 'Demo.Parent_Items/Demo.Item' + expect(compiler.send(:association_source_entity, path)).to eq('Demo.Parent') + operation = compiler.send(:association_retrieve_operation, 'Demo.Home', widget, [], path) + expect(operation).to include('operationType' => 'retrieve', + 'parameters' => { 'CurrentObject' => ['Demo.Parent'] }) + expect(compiler.send(:association_operation, 'Demo.Home', widget, [], + instance_double(Mxrb::Compiler::WebListDataSource, association_path: path))) + .to include('operationType' => 'retrieve') + + list_source = instance_double( + Mxrb::Compiler::WebListDataSource, association?: true, association_path: path + ) + allow(Mxrb::Compiler::WebListDataSource).to receive(:new).and_return(list_source) + expect(compiler.send(:standard_data_source_operation, 'Demo.Home', { + '$Type' => 'Forms$ListView', 'Name' => 'list' + }, [], {})).to include('operationType' => 'retrieve') + allow(Mxrb::Compiler::WebListDataSource).to receive(:new).and_call_original + expect(compiler.send(:standard_data_source_operation, 'Demo.Home', { + '$Type' => 'Forms$ListView', 'Name' => 'flowList', 'DataSource' => { + '$Type' => 'Forms$MicroflowSource', 'MicroflowSettings' => { 'Microflow' => 'Demo.Load' } + } + }, [], {})).to include('operationType' => 'retrieveByMicroflow') + + nested = { 'AttributeRef' => { 'Attribute' => 'Demo.Item.Name', 'EntityRef' => { + 'Steps' => [2, { 'Association' => 'Demo.Parent_Items', 'DestinationEntity' => 'Demo.Item' }] + } } } + expect(compiler.send(:used_associations, nested, 'Demo.Parent')) + .to eq(['Demo.Parent/Demo.Parent_Items/Demo.Item']) + expect(compiler.send(:model_id, SecureRandom.uuid)).to be_a(String) + + flow.document['MicroflowReturnType'] = { 'Entity' => 'Demo.Item' } + expect(compiler.send(:microflow_return_entity, 'MicroflowSettings' => { + 'Microflow' => 'Demo.Load' + })).to eq('Demo.Item') + + menu_operations = compiler.send(:menu_item_operations, { + 'Action' => { '$ID' => SecureRandom.uuid, '$Type' => 'Forms$MicroflowAction', + 'MicroflowSettings' => { 'Microflow' => 'Demo.Load' } }, 'Items' => [2] + }) + expect(menu_operations).to contain_exactly(include('operationType' => 'callMicroflow')) + + association_source = instance_double( + Mxrb::Compiler::WebListDataSource, + supported?: true, xpath?: false, association?: true, association_path: path + ) + allow(Mxrb::Compiler::WebListDataSource).to receive(:new).and_return(association_source) + expect(compiler.send(:operation, 'Demo.Home', widget, [], {})) + .to include('operationType' => 'retrieve') + + grid_object = { 'kind' => 'grid' } + grid = { 'Object' => grid_object } + column = { 'kind' => 'column' } + filter = { 'Name' => 'category' } + allow(compiler).to receive(:custom_property_values) do |object| + if object&.[]('kind') == 'grid' + { 'columns' => ['Object', { 'Objects' => [column] }] } + else + { + 'attribute' => ['Attribute', { 'AttributeRef' => { + 'Attribute' => 'Demo.Category.Name', 'EntityRef' => { 'Steps' => [{ + 'DestinationEntity' => 'Demo.Category' + }] } + } }], + 'filter' => ['Widgets', { 'Widgets' => [filter] }] + } + end + end + expect(compiler.send(:grid_filter_specs, grid)).to eq([[filter, 'Demo.Category', 'Name']]) + allow(compiler).to receive(:custom_property_values) do |object| + if object&.[]('kind') == 'grid' + { 'columns' => ['Object', { 'Objects' => [column] }] } + else + { + 'attribute' => ['Attribute', { 'AttributeRef' => { + 'Attribute' => '', 'EntityRef' => { 'Steps' => [{ 'DestinationEntity' => '' }] } + } }], + 'filter' => ['Widgets', { 'Widgets' => [filter] }] + } + end + end + expect(compiler.send(:grid_filter_specs, grid)).to eq([]) + end + + it 'handles a page whose referenced popup layout is absent' do + Dir.mktmpdir do |root| + mpr = File.join(root, 'Popup.mpr') + Mxrb.define(mpr) do + mendix_version '11.12.1' + self.module(:Demo) { page(:Home) { layout 'Demo.Missing' } } + end + source = Mxrb::Compiler::SourceModel.read(mpr) + page = source.units_of('Forms$Page').first + expect(described_class.new(source).send(:popup_page?, page)).to be(false) + end + end end RSpec.describe Mxrb::Compiler::DataGridBundleCompiler do @@ -431,5 +659,116 @@ def source(widget_schema = schema) ) expect(compiler.send(:attribute_type, 'Other.Item', 'Name')).to eq('String') end + + it 'compiles dynamic, custom-content, and association-filter column branches' do # rubocop:disable Metrics/BlockLength + compiler = described_class.new( + source, 'Demo.Home', grid, + render_widgets: ->(widgets, scope, entity) { JSON.generate([widgets.length, scope, entity]) } + ) + index = compiler.instance_variable_get(:@index) + { + 'dynamic' => %w[dynamicText TextTemplate], + 'content' => %w[content Widgets], + 'filter' => %w[filter Widgets], + 'multi' => %w[multiSelect Boolean] + }.each do |id, (key, type)| + index[id] = property_type(id, key, type) + end + + date_domain = unit(module_name: 'Demo', document: { + 'Entities' => [2, { 'Name' => 'Item', 'Attributes' => [2, { + 'Name' => 'CreatedAt', 'NewType' => { '$Type' => 'DomainModels$DateTimeAttributeType' } + }] }] + }) + allow(compiler.instance_variable_get(:@source)).to receive(:units_of) + .with('DomainModels$DomainModel').and_return([date_domain]) + + dynamic_values = { + 'dynamicText' => ['TextTemplate', { 'TextTemplate' => { 'Parameters' => [2, {}] } }] + } + expect(compiler.send(:dynamic_text, dynamic_values, 'Demo.Item', 'CreatedAt').fetch('$raw')) + .to include('_format') + expect(compiler.send(:dynamic_text, dynamic_values, 'Demo.Item', 'Name').fetch('$raw')) + .not_to include('_format') + expect(compiler.send(:dynamic_text, {}, 'Demo.Item', '')).to eq(:undefined) + + dynamic_column = { + 'Properties' => [2, + property('show', value('show-value', primitive: 'dynamicText')), + property('attribute', value('attribute-value', AttributeRef: { + 'Attribute' => 'Demo.Item.CreatedAt' + })), + property('dynamic', value('dynamic-value', TextTemplate: { + 'Parameters' => [2, {}] + }))] + } + expect(compiler.send(:compile_column, dynamic_column).fetch(:dynamicText).fetch('$raw')) + .to include('_format') + + content_values = { 'content' => ['Widgets', { 'Widgets' => [2, { 'Name' => 'Child' }] }] } + expect(compiler.send(:templated_content, content_values).fetch('$raw')).to include('TemplatedWidgetProperty') + expect(compiler.send(:compile_widgets, nil)).to eq([]) + expect(compiler.send(:compile_widgets, 'Widgets' => [2, { 'Name' => 'Filter' }]).fetch('$raw')) + .to include('p.Demo.Home.grid') + expect(compiler.send(:filter_widget, {})).to be_nil + expect(compiler.send(:filter_widget, 'filter' => [])).to be_nil + expect(compiler.send(:templated_content, {})).to include('$raw' => include('TemplatedWidgetProperty')) + expect(compiler.send(:templated_content, 'content' => [])).to include( + '$raw' => include('TemplatedWidgetProperty') + ) + + multi_select = property('multi', value('multi-value', primitive: 'true')) + filter = { 'Name' => 'associationFilter', + 'Object' => { 'Properties' => [2, multi_select] } } + association_values = { + 'attribute' => ['Attribute', { 'AttributeRef' => { + 'Attribute' => 'Demo.Item.Name', 'EntityRef' => { 'Steps' => [2, { + 'Association' => 'Demo.Item_Category', 'DestinationEntity' => 'Demo.Category' + }] } + } }], + 'filter' => ['Widgets', { 'Widgets' => [2, filter] }] + } + expect(compiler.send(:association_filter?, association_values)).to be(true) + expect(compiler.send(:association_filter, association_values).fetch(:filterAssociation).fetch('$raw')) + .to include('ReferenceSet') + + single_filter = { 'Name' => 'singleFilter', 'Object' => { 'Properties' => [2] } } + single_association_values = Marshal.load(Marshal.dump(association_values)) + single_association_values['filter'].last['Widgets'][1] = single_filter + expect(compiler.send(:association_filter, single_association_values).fetch(:filterAssociation).fetch('$raw')) + .to include('"type": "Reference"') + expect(compiler.send(:compile_column, { + 'Properties' => [2, + property('show', value('show-value', primitive: 'attribute')), + property('attribute', association_values['attribute'].last), + property('filter', single_association_values['filter'].last)] + })).to include(:filterAssociation, :filterAssociationOptions, :filterAssociationOptionLabel) + expect(compiler.send(:filter_boolean, { 'Object' => nil }, 'multiSelect')).to be(false) + expect(compiler.send(:filter_boolean, { + 'Object' => { 'Properties' => [2, property('multi', nil)] } + }, 'multiSelect')).to be(false) + + custom = { + 'Properties' => [2, + property('show', value('show-value', primitive: 'customContent')), + property('content', value('content-value', Widgets: [2, { 'Name' => 'Child' }]))] + } + expect(compiler.send(:supported_column?, custom)).to be(true) + expect(compiler.send(:compile_column, custom).fetch(:content).fetch('$raw')) + .to include('TemplatedWidgetProperty') + compiler.instance_variable_set(:@render_widgets, nil) + expect(compiler.send(:supported_column?, custom)).to be(false) + expect(compiler.send(:supported_column?, 'Properties' => [2])).to be(false) + compiler.instance_variable_set(:@render_widgets, ->(*) { '[]' }) + custom_without_content = { + 'Properties' => [2, property('show', value('show-value', primitive: 'customContent'))] + } + expect(compiler.send(:supported_column?, custom_without_content)).to be(false) + custom_with_nil_content = { + 'Properties' => [2, property('show', value('show-value', primitive: 'customContent')), + property('content', nil)] + } + expect(compiler.send(:supported_column?, custom_with_nil_content)).to be(false) + end end # rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/MethodLength diff --git a/spec/widget_package_spec.rb b/spec/widget_package_spec.rb index fc12556..1e96121 100644 --- a/spec/widget_package_spec.rb +++ b/spec/widget_package_spec.rb @@ -30,14 +30,17 @@ def widget_xml(id: 'example.Widget') Subtitle Formula - + Attribute - + Action + Empty action @@ -92,6 +95,13 @@ def array(value) = Mxrb::IO::BsonCodec.parse_array(value)[:items] expect(by_key.dig('mode', 'EnumerationValues')).not_to eq([2]) expect(by_key.dig('attribute', 'AllowedTypes')).to eq([1, 'String']) expect(by_key.dig('attribute', 'AssociationTypes')).to eq([1, 'Reference']) + expect(by_key.fetch('attribute')).to include( + 'IsLinked' => true, 'IsMetaData' => true, 'SetLabel' => true, + 'SelectableObjectsProperty' => 'source' + ) + expect(by_key.dig('action', 'DefaultType')).to eq('CallNanoflow') + expect(array(by_key.dig('action', 'ActionVariables')).first) + .to include('Caption' => 'Input', 'Key' => 'input', 'Type' => 'String') expect(by_key.dig('action', 'ReturnType')).to include('Type' => 'Object', 'AssignableTo' => 'context') expect(by_key.dig('emptyAction', 'ReturnType')).to include('Type' => 'None') expect(array(by_key.dig('rows', 'ObjectType', 'PropertyTypes')).first['PropertyKey']).to eq('label') @@ -132,6 +142,29 @@ def array(value) = Mxrb::IO::BsonCodec.parse_array(value)[:items] end end + it 'reads direct nested properties without requiring an extra property group' do + xml = <<~XML + Direct + Filters + Options + Label + + + + + XML + Dir.mktmpdir do |dir| + path = File.join(dir, 'direct.mpk') + write_package(path, xml:) + definition = described_class.new(path).definition('direct.Widget') + filters = definition.properties.fetch(0) + options = filters.fetch(:children).fetch(0) + expect(filters[:children].map { _1[:key] }).to eq(['filterOptions']) + expect(options[:children].map { _1[:key] }).to eq(['label']) + expect(options[:category]).to eq('General') + end + end + it 'uses an installed widget schema directly when the writer creates a page widget' do Dir.mktmpdir do |dir| widgets = File.join(dir, 'widgets') diff --git a/spec/writer_modern_edge_spec.rb b/spec/writer_modern_edge_spec.rb index a123db0..f6a3702 100644 --- a/spec/writer_modern_edge_spec.rb +++ b/spec/writer_modern_edge_spec.rb @@ -19,6 +19,149 @@ writer.send(:ensure_project_documents, mpr, 'root') end + it 'removes dangling template demo users while bootstrapping project roles' do + raw = { 'UnitID' => 'security', 'ContainmentName' => 'ProjectDocuments' } + existing = { + '$ID' => 'security', '$Type' => 'Security$ProjectSecurity', + 'UserRoles' => [2], 'EnableDemoUsers' => true, + 'DemoUsers' => [2, { + '$Type' => 'Security$DemoUserImpl', 'UserName' => 'demo_user', + 'Entity' => 'Administration.Account', 'UserRoles' => [1, 'User'] + }] + } + mpr = double(children_of: [raw]) + allow(mpr).to receive(:parse_contents).with(raw).and_return(existing) + expect(mpr).to receive(:update_unit).with( + 'security', hash_including( + 'EnableDemoUsers' => false, 'DemoUsers' => [2], 'SecurityLevel' => 'CheckNothing' + ) + ) + + writer.send(:write_project_security, mpr, 'root', {}) + end + + it 'removes donor lifecycle callbacks from standalone project templates' do + document = { + 'Settings' => [2, + { '$Type' => 'Forms$WebUIProjectSettingsPart', + 'ThemeModuleName' => 'Donor_UI' }, + { '$Type' => 'Settings$ModelSettings', + 'AfterStartupMicroflow' => 'Donor.Start', + 'BeforeShutdownMicroflow' => 'Donor.Stop', + 'HealthCheckMicroflow' => 'Donor.Health' }] + } + + writer.send(:sanitize_project_settings!, document) + + expect(document['Settings'][1]).to include( + 'EnableNewStringBehavior' => true, 'ThemeModuleName' => '' + ) + expect(document['Settings'][2]).to include( + 'AfterStartupMicroflow' => '', 'BeforeShutdownMicroflow' => '', + 'HealthCheckMicroflow' => '' + ) + end + + it 'leaves absent lifecycle fields absent and uses the prior legacy application title' do + document = { 'Settings' => [2, { '$Type' => 'Settings$ModelSettings' }] } + writer.send(:sanitize_project_settings!, document) + expect(document['Settings'][1]).not_to have_key('AfterStartupMicroflow') + + profile = writer.send( + :legacy_navigation_profile_doc, + { role_homes: {}, role_home_details: [], app_title: {}, items: [] }, + previous: { 'ApplicationTitle' => 'Existing title' } + ) + expect(profile['ApplicationTitle']).to eq('Existing title') + end + + it 'normalizes explicitly supplied native layouts before writing Mendix 6 documents' do + legacy = described_class.new('v6.mpr', version: '6.10.8', modules: []) + mpr = double + expect(legacy).to receive(:upsert_native_unit).with( + mpr, 'module-id', hash_including( + 'containment' => 'Documents', + 'doc' => hash_including('$Type' => 'Forms$Layout', 'Widget' => hash_including( + '$Type' => 'Forms$Placeholder' + )) + ) + ) + legacy.send( + :write_native_documents, mpr, 'module-id', + native_documents: [{ containment: 'Documents', doc: { + '$Type' => 'Forms$Layout', 'Name' => 'Shell' + } }] + ) + end + + it 'removes donor image references from standalone navigation templates' do + document = { + 'Profiles' => [2, { + '$Type' => 'Navigation$NavigationProfile', 'Name' => 'Responsive', + 'AppIcon' => 'Donor_UI.Images.Icon' + }] + } + + writer.send(:sanitize_project_navigation!, document) + + expect(document.dig('Profiles', 1, 'AppIcon')).to eq('') + end + + it 'materializes the audited singular and plural legacy layout contracts' do + source = { 'Name' => 'Shell', 'Appearance' => { 'Class' => 'shell', 'Style' => 'gap: 1px' } } + oldest = described_class.new('v6.mpr', version: '6.10.8', modules: []) + .send(:legacy_layout_doc, source) + modern_legacy = described_class.new('v717.mpr', version: '7.17.0', modules: []) + .send(:legacy_layout_doc, source.merge('$ID' => 'layout-id')) + + expect(oldest).to include('MainPlaceholderName' => 'Main', 'Widget' => hash_including( + '$Type' => 'Forms$Placeholder' + )) + expect(oldest).not_to have_key('Widgets') + expect(modern_legacy).to include('$ID' => 'layout-id', 'Widgets' => [2, hash_including( + '$Type' => 'Forms$Placeholder' + )]) + end + + it 'normalizes nested legacy widgets and singular cardinalities' do + legacy = described_class.new('v6.mpr', version: '6.10.8', modules: []) + tree = { + '$Type' => 'Forms$DataView', 'Other' => 'kept', 'Metadata' => { 'Flag' => true }, + 'Widgets' => [2, { '$Type' => 'Forms$DivContainer', 'Widgets' => [2] }, + { '$Type' => 'Forms$TabPage', 'Widgets' => [2, { 'Name' => 'Only' }] }], + 'FooterWidgets' => [2, { 'Name' => 'First' }, { 'Name' => 'Second' }] + } + + normalized = legacy.send(:legacy_widget_tree, tree) + expect(normalized['Widget']['$Type']).to eq('Forms$VerticalFlow') + expect(normalized['Widget']['Widgets'][1]['Widget']).to be_nil + expect(normalized['Widget']['Widgets'][2]['Widget']).to eq('Name' => 'Only') + expect(normalized['FooterWidget']['$Type']).to eq('Forms$VerticalFlow') + expect(normalized['Other']).to eq('kept') + expect(normalized['Metadata']).to eq('Flag' => true) + expect(legacy.send(:legacy_widget_tree, '$Type' => 'Forms$DivContainer')) + .not_to have_key('Widget') + end + + it 'covers legacy navigation and singular page decisions without donor references' do + legacy = described_class.new('v6.mpr', version: '6.10.8', modules: []) + navigation = legacy.send(:legacy_navigation_doc, {}, profiles: [{ + name: 'Responsive', home_page: nil, home_microflow: nil, + role_homes: { 'User' => 'App.Home' }, role_home_details: [], + app_title: 'Plain title', items: [], kind: nil + }]) + page = legacy.send(:page_doc, { + name: 'Home', layout: 'App.Shell', title: 'Home', widgets: [], events: [], + allowed_roles: nil, popup: false, data_source: nil + }) + + expect(navigation).to have_key('DesktopProfile') + expect(navigation.dig('DesktopProfile', 'ApplicationTitle')).to eq('Plain title') + expect(navigation.dig('DesktopProfile', 'HomeItems', 1, 'Page')).to eq('App.Home') + expect(navigation.fetch('DesktopProfile')).not_to have_key('Kind') + expect(page.dig('FormCall', 'Arguments', 1)).to have_key('Widget') + end + it 'inserts absent security and navigation project documents' do mpr = double(children_of: []) expect(mpr).to receive(:insert_unit).with( @@ -113,6 +256,47 @@ expect(objects.map { _1['$Type'] }).to include('Microflows$Annotation') end + it 'preserves localized native validation rules until explicitly disabled' do + native_rule = { + '$ID' => 'rule', '$Type' => 'DomainModels$ValidationRule', + 'Attribute' => 'Catalog.Item.Name', + 'Message' => { + '$ID' => 'message', '$Type' => 'Texts$Text', + 'Items' => Mxrb::IO::BsonCodec.build_array([ + { 'LanguageCode' => 'en_US', + 'Text' => 'Name is required' }, + { 'LanguageCode' => 'nl_NL', + 'Text' => 'Naam is verplicht' } + ]) + }, + 'RuleInfo' => { '$ID' => 'info', '$Type' => 'DomainModels$RequiredRuleInfo' } + } + previous = Mxrb::IO::BsonCodec.build_array([native_rule]) + + preserved = writer.send( + :validation_rules_doc, + { name: 'Item', attributes: [{ name: 'Name', required: true }] }, 'Catalog', previous + ) + expect(Mxrb::IO::BsonCodec.parse_array(preserved)[:items]).to eq([native_rule]) + + removed = writer.send( + :validation_rules_doc, + { name: 'Item', attributes: [{ name: 'Name', required: false }] }, 'Catalog', previous + ) + expect(Mxrb::IO::BsonCodec.parse_array(removed)[:items]).to be_empty + + generated = writer.send( + :validation_rules_doc, + { name: 'Item', attributes: [{ name: 'Name', unique: true }] }, 'Catalog', previous + ) + expect(Mxrb::IO::BsonCodec.parse_array(generated)[:items]).to contain_exactly( + native_rule, hash_including( + 'Attribute' => 'Catalog.Item.Name', + 'RuleInfo' => hash_including('$Type' => 'DomainModels$UniqueRuleInfo') + ) + ) + end + it 'writes supported schedules and rejects unsupported intervals and units' do expect(writer.send(:scheduled_event_schedule_doc, unit: :minutes, interval: 5)) .to include('$Type' => 'ScheduledEvents$MinuteSchedule', 'Multiplier' => 5)