mxrb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,367 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module OfficialMarketplace
5
+ # Immutable preview whose mutation is guarded by explicit apply and blockers.
6
+ class LifecyclePlan
7
+ attr_reader :action, :name, :installed_version, :target_version, :changes, :blockers
8
+
9
+ # rubocop:disable Metrics/ParameterLists
10
+ def initialize(action:, name:, installed_version:, target_version:, changes:, blockers:, &operation)
11
+ @action = action
12
+ @name = name
13
+ @installed_version = installed_version
14
+ @target_version = target_version
15
+ @changes = changes.freeze
16
+ @blockers = blockers.freeze
17
+ @operation = operation
18
+ @applied = false
19
+ end
20
+ # rubocop:enable Metrics/ParameterLists
21
+
22
+ def safe? = blockers.empty?
23
+ def applied? = @applied
24
+
25
+ def apply!
26
+ raise MarketplaceError, 'Marketplace lifecycle plan was already applied' if applied?
27
+ raise MarketplaceError, "Marketplace lifecycle plan is blocked: #{blockers.join('; ')}" unless safe?
28
+
29
+ @operation.call
30
+ @applied = true
31
+ self
32
+ end
33
+ end
34
+
35
+ # Read-only view of a module package used to compare update boundaries.
36
+ class ModulePackageInventory
37
+ attr_reader :name, :version, :module_id, :unit_ids, :files
38
+
39
+ def initialize(name:, version:, module_id:, unit_ids:, files:)
40
+ @name = name
41
+ @version = version
42
+ @module_id = module_id
43
+ @unit_ids = unit_ids.freeze
44
+ @files = files.freeze
45
+ end
46
+
47
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
48
+ def self.read(path)
49
+ Dir.mktmpdir('mxrb-marketplace-inspect-') do |temporary|
50
+ Zip::File.open(path) do |archive|
51
+ reader = ModulePackageReader.new(archive)
52
+ descriptor = reader.descriptor
53
+ source_path = reader.extract_project(descriptor, temporary)
54
+ staged = reader.stage_files(descriptor.files, temporary)
55
+ source = IO::MprFile.open(source_path, readonly: true)
56
+ module_unit = find_module(source, descriptor.name)
57
+ raise MarketplaceError, "module #{descriptor.name.inspect} is absent from package MPR" unless module_unit
58
+
59
+ ids = subtree_ids(source, module_unit.fetch('UnitID'))
60
+ files = staged.transform_values { Digest::SHA256.file(_1).hexdigest }
61
+ return new(
62
+ name: descriptor.name, version: descriptor.version,
63
+ module_id: module_unit.fetch('UnitID'), unit_ids: ids, files:
64
+ )
65
+ ensure
66
+ source&.close
67
+ end
68
+ end
69
+ rescue Zip::Error, REXML::ParseException => e
70
+ raise MarketplaceError, "invalid Mendix module package: #{e.message}"
71
+ end
72
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
73
+
74
+ def self.find_module(mpr, name)
75
+ mpr.units_by_containment('Modules').find { mpr.parse_contents(_1)['Name'] == name }
76
+ end
77
+
78
+ def self.subtree_ids(mpr, root_id)
79
+ ids = [root_id]
80
+ loop do
81
+ children = mpr.all_units.filter_map do |unit|
82
+ unit.fetch('UnitID') if ids.include?(unit['ContainerID']) && !ids.include?(unit.fetch('UnitID'))
83
+ end
84
+ break if children.empty?
85
+
86
+ ids.concat(children)
87
+ end
88
+ ids
89
+ end
90
+ end
91
+
92
+ # Transactional lifecycle for modules already recorded in marketplace.lock.json.
93
+ class Lifecycle # rubocop:disable Metrics/ClassLength
94
+ def initialize(target:, mpr:, installer:)
95
+ @target = File.expand_path(target)
96
+ @mpr = mpr && File.expand_path(mpr)
97
+ @installer = installer
98
+ end
99
+
100
+ def installed(identifier)
101
+ pair = packages.find do |name, entry|
102
+ name.casecmp(identifier.to_s).zero? || entry['content_id'].to_s == identifier.to_s
103
+ end
104
+ raise MarketplaceError, "Marketplace package #{identifier.inspect} is not installed" unless pair
105
+ raise MarketplaceError, "Marketplace package #{pair.first.inspect} is not an imported module" \
106
+ unless pair.last['kind'] == 'module'
107
+
108
+ pair
109
+ end
110
+
111
+ def mpr_path(entry)
112
+ path = safe_path(entry.fetch('destination'))
113
+ raise MarketplaceError, 'selected MPR does not match the Marketplace lock' if @mpr && @mpr != path
114
+
115
+ path
116
+ end
117
+
118
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
119
+ def plan_remove(identifier)
120
+ name, entry = installed(identifier)
121
+ mpr = mpr_path(entry)
122
+ ids = target_unit_ids(mpr, entry)
123
+ inventory = cached_inventory(entry)
124
+ shared = shared_files(name)
125
+ blockers = base_blockers(entry, inventory, ids)
126
+ blockers << "locked module name does not match cached package #{inventory.name}" if inventory.name != name
127
+ blockers.concat(external_reference_blockers(mpr, ids))
128
+ blockers.concat(modified_asset_blockers(inventory, name))
129
+ removable = inventory.files.keys - shared
130
+ changes = ["delete #{ids.size} MPR units", "delete #{removable.size} package assets",
131
+ 'delete cached package and lock entry']
132
+ LifecyclePlan.new(
133
+ action: :remove, name:, installed_version: entry['version'], target_version: nil,
134
+ changes:, blockers:
135
+ ) { apply_remove(name, entry, mpr, ids, removable) }
136
+ end
137
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
138
+
139
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
140
+ def plan_update(identifier, archive, package)
141
+ name, entry = installed(identifier)
142
+ mpr = mpr_path(entry)
143
+ current_ids = target_unit_ids(mpr, entry)
144
+ current = cached_inventory(entry)
145
+ replacement = ModulePackageInventory.read(archive)
146
+ missing_ids = current_ids - replacement.unit_ids
147
+ blockers = base_blockers(entry, current, current_ids)
148
+ blockers << "replacement module name changed from #{name} to #{replacement.name}" if replacement.name != name
149
+ blockers << 'replacement module identity changed' if replacement.module_id != entry['module_id']
150
+ blockers << "downloaded package version #{replacement.version} does not match #{package.version}" \
151
+ unless package_version_matches?(replacement.version, package.version)
152
+ blockers << "version #{package.version} is already installed" if package.version.to_s == entry['version'].to_s
153
+ blockers.concat(external_reference_blockers(mpr, missing_ids))
154
+ blockers.concat(modified_asset_blockers(current, name))
155
+ obsolete = current.files.keys - replacement.files.keys - shared_files(name)
156
+ changes = ["replace #{current_ids.size} MPR units with #{replacement.unit_ids.size}",
157
+ "install #{replacement.files.size} package assets",
158
+ "delete #{obsolete.size} obsolete package assets", 'replace cache and lock metadata']
159
+ LifecyclePlan.new(
160
+ action: :update, name:, installed_version: entry['version'], target_version: package.version,
161
+ changes:, blockers:
162
+ ) { apply_update(entry, mpr, current_ids, obsolete, archive, package, replacement) }
163
+ end
164
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
165
+
166
+ private
167
+
168
+ def packages = OfficialMarketplace.lock(@target).fetch('packages')
169
+
170
+ def safe_path(relative)
171
+ path = File.expand_path(File.join(@target, relative.to_s))
172
+ prefix = "#{@target}#{File::SEPARATOR}"
173
+ raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix)
174
+
175
+ path
176
+ end
177
+
178
+ def cached_inventory(entry)
179
+ archive = safe_path(entry.fetch('archive'))
180
+ raise MarketplaceError, "cached Marketplace package is missing: #{archive}" unless File.file?(archive)
181
+
182
+ ModulePackageInventory.read(archive)
183
+ end
184
+
185
+ def target_unit_ids(mpr_path, entry)
186
+ raise MarketplaceError, "MPR not found: #{mpr_path}" unless File.file?(mpr_path)
187
+
188
+ mpr = IO::MprFile.open(mpr_path, readonly: true)
189
+ root = mpr.unit(entry.fetch('module_id'))
190
+ raise MarketplaceError, "module #{entry.fetch('module_id')} is absent from target MPR" unless root
191
+
192
+ ModulePackageInventory.subtree_ids(mpr, root.fetch('UnitID'))
193
+ ensure
194
+ mpr&.close
195
+ end
196
+
197
+ def base_blockers(entry, inventory, ids)
198
+ blockers = []
199
+ blockers << 'locked module identity does not match cached package' if inventory.module_id != entry['module_id']
200
+ blockers << 'locked unit count does not match target module tree' if entry['units'].to_i != ids.size
201
+ blockers
202
+ end
203
+
204
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
205
+ def external_reference_blockers(mpr_path, ids)
206
+ return [] if ids.empty?
207
+
208
+ id_set = ids.map(&:downcase)
209
+ mpr = IO::MprFile.open(mpr_path, readonly: true)
210
+ mpr.all_units.filter_map do |unit|
211
+ next if id_set.include?(unit.fetch('UnitID').downcase)
212
+
213
+ document = mpr.parse_contents(unit)
214
+ matches = id_set.select { JSON.generate(document).downcase.include?(_1) }
215
+ next if matches.empty?
216
+
217
+ "#{document['Name'] || document['$Type'] || unit.fetch('UnitID')} references " \
218
+ "#{matches.size} unit(s) scheduled for removal"
219
+ end
220
+ ensure
221
+ mpr&.close
222
+ end
223
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
224
+
225
+ def modified_asset_blockers(inventory, owner_name)
226
+ shared = shared_files(owner_name)
227
+ inventory.files.filter_map do |relative, expected|
228
+ next if shared.include?(relative)
229
+
230
+ path = safe_path(relative)
231
+ actual = Digest::SHA256.file(path).hexdigest if File.file?(path) && !File.symlink?(path)
232
+ "package asset changed or missing: #{relative}" unless actual == expected
233
+ end
234
+ end
235
+
236
+ def shared_files(name)
237
+ packages.filter_map do |other_name, entry|
238
+ Array(entry['files']) unless other_name == name
239
+ end.flatten.uniq
240
+ end
241
+
242
+ def package_version_matches?(manifest_version, official_version)
243
+ value = manifest_version.to_s
244
+ value.empty? || value == 'unknown' || value == official_version.to_s
245
+ end
246
+
247
+ def apply_remove(name, entry, mpr, ids, files)
248
+ paths = lifecycle_paths(entry, mpr, files)
249
+ with_rollback(paths) do
250
+ delete_units(mpr, ids)
251
+ restore_assets(entry, files)
252
+ remove_atlas_variables if name == 'Atlas_Core'
253
+ FileUtils.rm_f(safe_path(entry.fetch('archive')))
254
+ delete_asset_backups(entry)
255
+ delete_lock_entry(name)
256
+ end
257
+ end
258
+
259
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
260
+ def apply_update(entry, mpr, ids, obsolete, archive, package, replacement)
261
+ new_cache = cache_path(replacement)
262
+ files = (Array(entry['files']) + replacement.files.keys).uniq
263
+ paths = lifecycle_paths(entry, mpr, files) + [new_cache]
264
+ with_rollback(paths.uniq) do
265
+ delete_units(mpr, ids)
266
+ @installer.send(:import_module, archive, package, mpr)
267
+ restore_assets(entry, obsolete)
268
+ delete_asset_backups(entry, obsolete)
269
+ old_cache = safe_path(entry.fetch('archive'))
270
+ FileUtils.rm_f(old_cache) unless old_cache == new_cache
271
+ end
272
+ end
273
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
274
+
275
+ def delete_units(mpr_path, ids)
276
+ mpr = IO::MprFile.open(mpr_path)
277
+ mpr.transaction { ids.reverse_each { mpr.delete_unit(_1) } }
278
+ ensure
279
+ mpr&.close
280
+ end
281
+
282
+ def lifecycle_paths(entry, mpr, files)
283
+ contents = File.join(File.dirname(mpr), 'mprcontents')
284
+ lock = File.join(@target, '.mxrb', 'marketplace.lock.json')
285
+ custom = File.join(@target, 'theme', 'web', 'custom-variables.scss')
286
+ originals = File.join(@target, '.mxrb', 'marketplace-originals')
287
+ [mpr, contents, lock, custom, originals, safe_path(entry.fetch('archive'))] + files.map { safe_path(_1) }
288
+ end
289
+
290
+ def cache_path(inventory)
291
+ version = inventory.version.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')
292
+ File.join(@target, '.mxrb', 'marketplace', "#{inventory.name}-#{version}.mpk")
293
+ end
294
+
295
+ def with_rollback(paths)
296
+ Dir.mktmpdir('mxrb-marketplace-rollback-') do |temporary|
297
+ snapshots = []
298
+ paths.uniq.each_with_index { |path, index| snapshots << snapshot(path, temporary, index) }
299
+ yield
300
+ rescue StandardError
301
+ snapshots.reverse_each { restore_snapshot(_1) }
302
+ raise
303
+ end
304
+ end
305
+
306
+ def snapshot(path, temporary, index)
307
+ backup = File.join(temporary, index.to_s)
308
+ type = snapshot_type(path)
309
+ FileUtils.cp_r(path, backup) if type == :directory
310
+ FileUtils.cp(path, backup) if type == :file
311
+ [path, backup, type]
312
+ end
313
+
314
+ def snapshot_type(path)
315
+ return :directory if File.directory?(path)
316
+ return :file if File.file?(path)
317
+
318
+ :missing
319
+ end
320
+
321
+ def restore_snapshot(snapshot)
322
+ path, backup, type = snapshot
323
+ FileUtils.rm_rf(path) if File.directory?(path)
324
+ FileUtils.rm_f(path) if File.file?(path)
325
+ return if type == :missing
326
+
327
+ FileUtils.mkdir_p(File.dirname(path))
328
+ type == :directory ? FileUtils.cp_r(backup, path) : FileUtils.cp(backup, path)
329
+ end
330
+
331
+ def delete_lock_entry(name)
332
+ path = File.join(@target, '.mxrb', 'marketplace.lock.json')
333
+ lock = OfficialMarketplace.lock(@target)
334
+ lock.fetch('packages').delete(name)
335
+ @installer.send(:write_lock_atomically, path, lock)
336
+ end
337
+
338
+ def restore_assets(entry, files) # rubocop:disable Metrics/MethodLength
339
+ originals = entry.fetch('asset_originals', {})
340
+ files.each do |relative|
341
+ destination = safe_path(relative)
342
+ backup = originals[relative]
343
+ if backup && File.file?(safe_path(backup))
344
+ FileUtils.mkdir_p(File.dirname(destination))
345
+ FileUtils.cp(safe_path(backup), destination)
346
+ else
347
+ FileUtils.rm_f(destination)
348
+ end
349
+ end
350
+ end
351
+
352
+ def delete_asset_backups(entry, files = nil)
353
+ originals = entry.fetch('asset_originals', {})
354
+ selected = files ? originals.slice(*files) : originals
355
+ selected.each_value { FileUtils.rm_f(safe_path(_1)) if _1 }
356
+ end
357
+
358
+ def remove_atlas_variables
359
+ path = File.join(@target, 'theme', 'web', 'custom-variables.scss')
360
+ return unless File.file?(path)
361
+
362
+ lines = File.readlines(path).reject { _1.strip == Installer::ATLAS_VARIABLES_IMPORT }
363
+ File.write(path, lines.join)
364
+ end
365
+ end
366
+ end
367
+ end
@@ -0,0 +1,318 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'rexml/document'
5
+ require 'tmpdir'
6
+ require 'zip'
7
+
8
+ module Mxrb
9
+ module OfficialMarketplace
10
+ # Parses and safely stages a Mendix module package archive.
11
+ class ModulePackageReader
12
+ Descriptor = Data.define(:name, :version, :model_version, :project_file, :files)
13
+
14
+ def initialize(archive)
15
+ @archive = archive
16
+ end
17
+
18
+ def descriptor
19
+ package_xml = @archive.find_entry('package.xml')
20
+ raise MarketplaceError, 'package.xml is missing' unless package_xml
21
+
22
+ document = REXML::Document.new(package_xml.get_input_stream.read)
23
+ build_descriptor(document, parse_manifest)
24
+ end
25
+
26
+ def extract_project(descriptor, temporary)
27
+ entry = @archive.find_entry(descriptor.project_file)
28
+ raise MarketplaceError, "package project not found: #{descriptor.project_file}" unless entry
29
+
30
+ copy_entry(entry, File.join(temporary, 'source.mpr'))
31
+ end
32
+
33
+ def stage_files(files, temporary)
34
+ root = File.join(temporary, 'assets')
35
+ files.to_h do |relative|
36
+ entry = @archive.find_entry(relative) || @archive.find_entry(relative.tr('/', '\\'))
37
+ raise MarketplaceError, "declared package file is missing: #{relative}" unless entry&.file?
38
+
39
+ [relative, copy_entry(entry, File.join(root, relative))]
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # Package metadata is intentionally validated as one boundary.
46
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
47
+ def build_descriptor(document, manifest)
48
+ module_element = element(document, 'module')
49
+ project_element = element(document, 'projectFile')
50
+ raise MarketplaceError, 'package does not contain a Mendix module' unless module_element
51
+ raise MarketplaceError, 'package does not declare project.mpr' unless project_element
52
+
53
+ name = valid_module_name(module_element.attributes['name'])
54
+ validate_package_type!(manifest.dig('package', 'type'))
55
+ Descriptor.new(
56
+ name, manifest.dig('package', 'version') || 'unknown', manifest['model-version'],
57
+ safe_relative_path(project_element.attributes['path']),
58
+ elements(document, 'file').map { safe_relative_path(_1.attributes['path']) }.uniq
59
+ )
60
+ end
61
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
62
+
63
+ def valid_module_name(value)
64
+ name = value.to_s
65
+ return name if name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/)
66
+
67
+ raise MarketplaceError, "invalid module name #{name.inspect}"
68
+ end
69
+
70
+ def validate_package_type!(value)
71
+ package_type = value.to_s
72
+ return if package_type.empty? || package_type.casecmp('Module').zero?
73
+
74
+ raise MarketplaceError, "unsupported marketplace package type #{package_type.inspect}"
75
+ end
76
+
77
+ def parse_manifest
78
+ entry = @archive.find_entry('manifest.json')
79
+ entry ? JSON.parse(entry.get_input_stream.read) : {}
80
+ rescue JSON::ParserError => e
81
+ raise MarketplaceError, "invalid module manifest: #{e.message}"
82
+ end
83
+
84
+ def elements(document, name)
85
+ found = []
86
+ document.root&.each_recursive { found << _1 if _1.is_a?(REXML::Element) && _1.name == name }
87
+ found
88
+ end
89
+
90
+ def element(document, name) = elements(document, name).first
91
+
92
+ def safe_relative_path(value)
93
+ path = value.to_s.tr('\\', '/')
94
+ parts = path.split('/')
95
+ unsafe = path.empty? || path.start_with?('/') || parts.include?('..') || parts.include?('')
96
+ raise MarketplaceError, "unsafe package path #{value.inspect}" if unsafe
97
+ raise MarketplaceError, "protected package path #{value.inspect}" if protected_path?(parts)
98
+
99
+ parts.join('/')
100
+ end
101
+
102
+ def protected_path?(parts)
103
+ %w[.git .mxrb mprcontents].include?(parts.first.downcase) ||
104
+ parts.last.downcase.end_with?('.mpr') && parts.length > 1
105
+ end
106
+
107
+ def copy_entry(entry, destination)
108
+ FileUtils.mkdir_p(File.dirname(destination))
109
+ File.open(destination, 'wb') { ::IO.copy_stream(entry.get_input_stream, _1) }
110
+ destination
111
+ end
112
+ end
113
+
114
+ # Installs declared package files with rollback support.
115
+ class ModulePackageAssets
116
+ def initialize(target_root, temporary)
117
+ @target_root = target_root
118
+ @backup_root = File.join(temporary, 'backups')
119
+ @changes = []
120
+ end
121
+
122
+ def install(staged_files, protected_files = [])
123
+ staged_files.each do |relative, source|
124
+ install_file(relative, source) unless protected_files.include?(relative)
125
+ end
126
+ end
127
+
128
+ def rollback
129
+ @changes.reverse_each do |destination, backup|
130
+ backup ? FileUtils.cp(backup, destination) : FileUtils.rm_f(destination)
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def install_file(relative, source)
137
+ destination = safe_destination(relative)
138
+ raise MarketplaceError, "asset destination is a symbolic link: #{relative}" if File.symlink?(destination)
139
+ raise MarketplaceError, "asset destination is a directory: #{relative}" if File.directory?(destination)
140
+
141
+ backup = backup_file(relative, destination)
142
+ @changes << [destination, backup]
143
+ FileUtils.mkdir_p(File.dirname(destination))
144
+ FileUtils.cp(source, destination)
145
+ end
146
+
147
+ def safe_destination(relative)
148
+ destination = File.expand_path(File.join(@target_root, relative))
149
+ prefix = "#{@target_root}#{File::SEPARATOR}"
150
+ raise MarketplaceError, "unsafe asset destination: #{relative}" unless destination.start_with?(prefix)
151
+
152
+ destination
153
+ end
154
+
155
+ def backup_file(relative, destination)
156
+ return unless File.file?(destination)
157
+
158
+ backup = File.join(@backup_root, relative)
159
+ FileUtils.mkdir_p(File.dirname(backup))
160
+ FileUtils.cp(destination, backup)
161
+ backup
162
+ end
163
+ end
164
+
165
+ # Imports a Mendix module package directly into an MPR without Studio Pro.
166
+ class ModulePackageImporter # rubocop:disable Metrics/ClassLength
167
+ Result = Data.define(
168
+ :module_name, :package_version, :module_id, :units, :files,
169
+ :source_version, :target_version
170
+ )
171
+
172
+ def initialize(package_path, mpr_path, target_root: nil, allow_model_upgrade: false,
173
+ protected_files: [])
174
+ @package_path = File.expand_path(package_path)
175
+ @mpr_path = File.expand_path(mpr_path)
176
+ @target_root = File.expand_path(target_root || File.dirname(@mpr_path))
177
+ @allow_model_upgrade = allow_model_upgrade
178
+ @protected_files = protected_files
179
+ end
180
+
181
+ def import!
182
+ validate_inputs!
183
+ Dir.mktmpdir('mxrb-module-import-') do |temporary|
184
+ Zip::File.open(@package_path) { import_archive(_1, temporary) }
185
+ end
186
+ rescue Zip::Error, REXML::ParseException => e
187
+ raise MarketplaceError, "invalid Mendix module package: #{e.message}"
188
+ end
189
+
190
+ private
191
+
192
+ def import_archive(archive, temporary)
193
+ reader = ModulePackageReader.new(archive)
194
+ descriptor = reader.descriptor
195
+ source_path = reader.extract_project(descriptor, temporary)
196
+ staged_files = reader.stage_files(descriptor.files, temporary)
197
+ import_project(source_path, descriptor, staged_files, temporary)
198
+ end
199
+
200
+ def validate_inputs!
201
+ raise MarketplaceError, "package not found: #{@package_path}" unless File.file?(@package_path)
202
+ raise MarketplaceError, "MPR not found: #{@mpr_path}" unless File.file?(@mpr_path)
203
+ raise MarketplaceError, "target root not found: #{@target_root}" unless File.directory?(@target_root)
204
+ end
205
+
206
+ def import_project(source_path, descriptor, staged_files, temporary) # rubocop:disable Metrics/MethodLength
207
+ source = IO::MprFile.open(source_path, readonly: true)
208
+ target = IO::MprFile.open(@mpr_path)
209
+ assets = ModulePackageAssets.new(@target_root, temporary)
210
+ imported_ids = []
211
+ import_transaction(source, target, descriptor, staged_files, assets, imported_ids)
212
+ import_result(source, target, descriptor, staged_files, imported_ids)
213
+ rescue StandardError
214
+ assets&.rollback
215
+ cleanup_v2_units(target, imported_ids)
216
+ raise
217
+ ensure
218
+ source&.close
219
+ target&.close
220
+ end
221
+
222
+ # rubocop:disable Metrics/ParameterLists
223
+ def import_transaction(source, target, descriptor, staged_files, assets, imported_ids)
224
+ validate_versions!(source, target, descriptor)
225
+ module_unit, units = package_units(source, descriptor.name)
226
+ validate_target!(target, descriptor.name, units)
227
+ target.transaction do
228
+ imported_ids.concat(insert_units(source, target, module_unit, units))
229
+ assets.install(staged_files, @protected_files)
230
+ end
231
+ end
232
+ # rubocop:enable Metrics/ParameterLists
233
+
234
+ def insert_units(source, target, module_unit, units)
235
+ units.map do |unit|
236
+ container = unit == module_unit ? target.root_unit.fetch('UnitID') : unit.fetch('ContainerID')
237
+ target.insert_unit(
238
+ container_uuid: container, containment_name: unit.fetch('ContainmentName'),
239
+ contents_doc: source.parse_contents(unit)
240
+ )
241
+ end
242
+ end
243
+
244
+ def import_result(source, target, descriptor, staged_files, imported_ids)
245
+ Result.new(
246
+ descriptor.name, descriptor.version, imported_ids.first, imported_ids.size,
247
+ staged_files.keys, source.mendix_version, target.mendix_version
248
+ )
249
+ end
250
+
251
+ def validate_versions!(source, target, descriptor)
252
+ package_version = descriptor.model_version.to_s
253
+ if !package_version.empty? && package_version != source.mendix_version
254
+ raise MarketplaceError,
255
+ "module manifest version #{package_version} does not match package MPR #{source.mendix_version}"
256
+ end
257
+ return if source.mendix_version == target.mendix_version
258
+ return if @allow_model_upgrade && forward_version?(source.mendix_version, target.mendix_version)
259
+
260
+ raise MarketplaceError,
261
+ "module uses Mendix #{source.mendix_version}, target uses #{target.mendix_version}; " \
262
+ 'pure-Ruby import requires matching model versions'
263
+ end
264
+
265
+ def forward_version?(source, target)
266
+ Gem::Version.new(numeric_version(target)) >= Gem::Version.new(numeric_version(source))
267
+ end
268
+
269
+ def numeric_version(value)
270
+ value.to_s[/\A\d+(?:\.\d+){0,3}/] || '0'
271
+ end
272
+
273
+ def package_units(source, module_name) # rubocop:disable Metrics/MethodLength
274
+ module_unit = find_module_unit(source, module_name)
275
+ raise MarketplaceError, "module #{module_name.inspect} is absent from package MPR" unless module_unit
276
+
277
+ units = [module_unit]
278
+ loop do
279
+ ids = units.map { _1.fetch('UnitID') }
280
+ children = source.all_units.select do |unit|
281
+ ids.include?(unit['ContainerID']) && !ids.include?(unit['UnitID'])
282
+ end
283
+ break if children.empty?
284
+
285
+ units.concat(children)
286
+ end
287
+ [module_unit, units]
288
+ end
289
+
290
+ def find_module_unit(source, module_name)
291
+ source.units_by_containment('Modules').find do |unit|
292
+ document = source.parse_contents(unit)
293
+ %w[Projects$ModuleImpl Projects$Module].include?(document['$Type']) &&
294
+ document['Name'] == module_name
295
+ end
296
+ end
297
+
298
+ def validate_target!(target, module_name, units)
299
+ duplicate = target.units_by_containment('Modules').any? do |unit|
300
+ target.parse_contents(unit)['Name'] == module_name
301
+ end
302
+ raise MarketplaceError, "module #{module_name} already exists in target MPR" if duplicate
303
+
304
+ existing_ids = target.all_units.to_h { [_1.fetch('UnitID'), true] }
305
+ collision = units.find { existing_ids[_1.fetch('UnitID')] }
306
+ return unless collision
307
+
308
+ raise MarketplaceError, "module unit ID already exists: #{collision.fetch('UnitID')}"
309
+ end
310
+
311
+ def cleanup_v2_units(target, ids)
312
+ return unless target&.format_version == :v2
313
+
314
+ Array(ids).each { FileUtils.rm_f(target.content_path('UnitID' => _1)) }
315
+ end
316
+ end
317
+ end
318
+ end