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,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ # Adds an application module to an existing Ruby-first project atomically.
5
+ class ModuleInitializer < Initializer
6
+ Result = Data.define(:root, :files, :project_file)
7
+
8
+ def initialize(name)
9
+ super(name, subject: :module)
10
+ end
11
+
12
+ def scaffold(into: Dir.pwd)
13
+ prepare(into)
14
+ install
15
+ Result.new(@module_root, module_files, @project_file)
16
+ ensure
17
+ cleanup
18
+ end
19
+
20
+ private
21
+
22
+ def prepare(into)
23
+ @project_root = File.expand_path(into)
24
+ @project_file = File.join(@project_root, 'project.rb')
25
+ raise ArgumentError, "#{@project_file}: project.rb not found" unless File.file?(@project_file)
26
+
27
+ @module_root = File.join(@project_root, 'modules', @module_name)
28
+ abort "#{@module_root}: directory already exists" if File.exist?(@module_root)
29
+
30
+ @staging = Dir.mktmpdir(".#{@module_name}.mxrb-module-", @project_root)
31
+ @project_staging = File.join(@project_root, ".project.rb.mxrb-#{Process.pid}")
32
+ write_module_scaffold(@staging)
33
+ stage_project
34
+ end
35
+
36
+ def stage_project
37
+ source = File.binread(@project_file)
38
+ File.binwrite(@project_staging, connect_module(source))
39
+ File.chmod(File.stat(@project_file).mode, @project_staging)
40
+ end
41
+
42
+ def install
43
+ FileUtils.mkdir_p(File.dirname(@module_root))
44
+ File.rename(@staging, @module_root)
45
+ @module_installed = true
46
+ File.rename(@project_staging, @project_file)
47
+ @completed = true
48
+ end
49
+
50
+ def module_files
51
+ relative_module_files.map { File.join(@module_root, _1) }.freeze
52
+ end
53
+
54
+ def connect_module(source)
55
+ raise ArgumentError, 'project.rb does not contain Mxrb.define' unless source.include?('Mxrb.define')
56
+
57
+ load_line = %(evaluate File.join(__dir__, "modules", "#{@module_name}", "module.rb"))
58
+ raise ArgumentError, "project.rb already loads #{@module_name}" if source.include?(load_line)
59
+
60
+ lines = source.lines
61
+ closing = lines.rindex { _1.strip == 'end' }
62
+ raise ArgumentError, 'project.rb does not contain a closing end' unless closing
63
+
64
+ indentation = lines.filter_map { |line| line[/\A\s+(?=evaluate File\.join\(__dir__, "modules")/] }.first || ' '
65
+ lines.insert(closing, "#{indentation}#{load_line}\n")
66
+ lines.join
67
+ end
68
+
69
+ def cleanup
70
+ FileUtils.rm_rf(@module_root) if @module_installed && !@completed
71
+ FileUtils.rm_rf(@staging) if @staging && File.exist?(@staging)
72
+ FileUtils.rm_f(@project_staging) if @project_staging && File.exist?(@project_staging)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module OfficialMarketplace
5
+ # Client and resolver for the documented Mendix Marketplace Content API.
6
+ class ContentApi # rubocop:disable Metrics/ClassLength
7
+ BASE_URL = 'https://marketplace-api.mendix.com/v1'
8
+ AUTHORIZED_HOSTS = %w[marketplace-api.mendix.com marketplace.mendix.com].freeze
9
+ CONTENT_LIMIT = 100
10
+ VERSION_LIMIT = 20
11
+ MENDIX_VERSION = /\A(?:\d{1,3}|1000)(?:\.(?:\d{1,3}|1000)){0,2}\z/
12
+ MENDIX_BUILD_VERSION = /\A(?:\d{1,3}|1000)(?:\.(?:\d{1,3}|1000)){2}\.\d{1,10}\z/
13
+ UUID = /\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i
14
+
15
+ def initialize(pat: nil, credentials: Credentials.new, client: HttpClient.new)
16
+ @pat = pat.to_s.strip
17
+ @pat = ENV['MXRB_MENDIX_PAT'].to_s.strip if @pat.empty?
18
+ @pat = credentials.pat.to_s.strip if @pat.empty?
19
+ raise MarketplaceError, missing_pat_message if @pat.empty?
20
+
21
+ @client = client
22
+ end
23
+
24
+ def search(name: nil, private: nil, approved: nil, published_since: nil, # rubocop:disable Metrics/ParameterLists
25
+ limit: 10, offset: 0)
26
+ parameters = {
27
+ name:, isPrivate: boolean_filter(private), isCompanyApproved: boolean_filter(approved),
28
+ publishedSince: date_filter(published_since), limit: bounded_integer(limit, 1..CONTENT_LIMIT),
29
+ offset: bounded_integer(offset, 0..)
30
+ }.compact
31
+ items(request('/content', parameters))
32
+ end
33
+
34
+ def content(content_id)
35
+ identifier = positive_integer(content_id, 'content ID')
36
+ request("/content/#{identifier}")
37
+ end
38
+
39
+ def versions(content_id, version_id: nil, mendix_version: nil, published_since: nil, # rubocop:disable Metrics/ParameterLists
40
+ limit: VERSION_LIMIT, offset: 0)
41
+ identifier = positive_integer(content_id, 'content ID')
42
+ parameters = {
43
+ versionId: uuid_filter(version_id),
44
+ supportedMendixVersion: mendix_version_filter(mendix_version),
45
+ publishedSince: date_filter(published_since),
46
+ limit: bounded_integer(limit, 1..VERSION_LIMIT), offset: bounded_integer(offset, 0..)
47
+ }.compact
48
+ items(request("/content/#{identifier}/versions", parameters))
49
+ end
50
+
51
+ def find(identifier)
52
+ return content(identifier) if identifier.to_s.match?(/\A\d+\z/)
53
+
54
+ candidates(identifier).each do |name|
55
+ matches = search(name:)
56
+ return matches.first if matches.one?
57
+ raise MarketplaceError, "Marketplace content name #{name.inspect} is ambiguous" \
58
+ if matches.length > 1
59
+ end
60
+ raise MarketplaceError, "Marketplace content not found: #{identifier.inspect}"
61
+ end
62
+
63
+ def resolve(identifier, version: nil, mendix_version: nil, allow_vulnerable: false)
64
+ selected_content = find(identifier)
65
+ selected_version = resolve_version(selected_content.fetch('contentId'), version, mendix_version)
66
+ ensure_compatible!(selected_version, mendix_version) if mendix_version
67
+ reject_vulnerable!(selected_version) unless allow_vulnerable
68
+ package(selected_content, selected_version)
69
+ end
70
+
71
+ def download(version_id, destination, download_url: nil)
72
+ identifier = uuid_filter(version_id)
73
+ url = download_url || "#{BASE_URL}/versions/#{identifier}/download"
74
+ uri = URI.parse(url)
75
+ trusted = AUTHORIZED_HOSTS.include?(uri.host)
76
+ @client.download(url, destination, authorization: trusted ? authorization : nil)
77
+ end
78
+
79
+ def all_versions(content_id, published_since: nil)
80
+ paginated(VERSION_LIMIT) do |limit, offset|
81
+ versions(content_id, published_since:, limit:, offset:)
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def request(path, parameters = {})
88
+ @client.json(url(path, parameters), authorization: authorization)
89
+ end
90
+
91
+ def authorization = "MxToken #{@pat}"
92
+
93
+ def url(path, parameters)
94
+ query = URI.encode_www_form(parameters)
95
+ query.empty? ? "#{BASE_URL}#{path}" : "#{BASE_URL}#{path}?#{query}"
96
+ end
97
+
98
+ def items(payload)
99
+ value = payload.fetch('items', [])
100
+ raise MarketplaceError, 'invalid Marketplace Content API response: items must be an array' \
101
+ unless value.is_a?(Array)
102
+
103
+ value
104
+ end
105
+
106
+ def resolve_version(content_id, version, mendix_version)
107
+ values = version_candidates(content_id, version, mendix_version)
108
+ raise MarketplaceError, version_not_found_message(version, mendix_version) if values.empty?
109
+
110
+ values.max_by { version_key(_1) }
111
+ end
112
+
113
+ def version_candidates(content_id, version, mendix_version)
114
+ return versions(content_id, version_id: version) if version&.match?(UUID)
115
+ return all_versions(content_id).select { _1['versionNumber'].to_s == version } if version
116
+ return versions(content_id, mendix_version:) if mendix_version
117
+
118
+ versions(content_id)
119
+ end
120
+
121
+ def package(content, version)
122
+ OfficialPackage.new(
123
+ version['name'] || content.dig('latestVersion', 'name') || "Content#{content['contentId']}",
124
+ version.fetch('versionNumber'), :mendix, version['downloadURL'] || version['downloadUrl'],
125
+ "content:#{content.fetch('contentId')}", content.fetch('contentId'),
126
+ version.fetch('versionId'), content.fetch('type'), version['versionType'] || 'Regular',
127
+ security_codes(version), content['isPrivate'], content['isCompanyApproved']
128
+ )
129
+ end
130
+
131
+ def reject_vulnerable!(version)
132
+ return unless version['versionType'] == 'Vulnerable'
133
+
134
+ codes = security_codes(version)
135
+ suffix = codes.empty? ? '' : " (#{codes.join(', ')})"
136
+ raise MarketplaceError,
137
+ "Marketplace version #{version['versionNumber']} is marked vulnerable#{suffix}; " \
138
+ 'select a security fix or pass --allow-vulnerable'
139
+ end
140
+
141
+ def ensure_compatible!(version, mendix_version)
142
+ minimum = version['minSupportedMendixVersion'].to_s
143
+ compatible = !minimum.empty? && Gem::Version.new(mendix_version) >= Gem::Version.new(minimum)
144
+ return if compatible
145
+
146
+ raise MarketplaceError,
147
+ "Marketplace version #{version['versionNumber']} requires Mendix #{minimum} or newer"
148
+ end
149
+
150
+ def security_codes(version)
151
+ issues = Array(version['vulnerabilities']) + Array(version['fixedSecurityIssues'])
152
+ issues.filter_map { _1['code'] }.uniq
153
+ end
154
+
155
+ def candidates(identifier)
156
+ name = identifier.to_s.strip
157
+ raise MarketplaceError, 'Marketplace content name must not be empty' if name.empty?
158
+
159
+ spaced = name.gsub(/([a-z\d])([A-Z])/, '\\1 \\2').tr('_-', ' ').squeeze(' ')
160
+ [name, spaced].uniq
161
+ end
162
+
163
+ def paginated(page_size)
164
+ offset = 0
165
+ result = []
166
+ loop do
167
+ page = yield(page_size, offset)
168
+ result.concat(page)
169
+ break if page.length < page_size
170
+
171
+ offset += page_size
172
+ end
173
+ result
174
+ end
175
+
176
+ def version_key(value)
177
+ [value['publicationDate'].to_s, Gem::Version.new(normalize_version(value['versionNumber']))]
178
+ end
179
+
180
+ def normalize_version(value)
181
+ value.to_s[/\A\d+(?:\.\d+){0,2}/] || '0'
182
+ end
183
+
184
+ def boolean_filter(value)
185
+ return if value.nil?
186
+ return value.to_s if [true, false].include?(value)
187
+ return value if %w[true false].include?(value)
188
+
189
+ raise MarketplaceError, "expected true or false, got #{value.inspect}"
190
+ end
191
+
192
+ def date_filter(value)
193
+ return if value.nil?
194
+
195
+ Date.iso8601(value.to_s).iso8601
196
+ rescue Date::Error
197
+ raise MarketplaceError, "invalid RFC 3339 full date #{value.inspect}"
198
+ end
199
+
200
+ def uuid_filter(value)
201
+ return if value.nil?
202
+ raise MarketplaceError, "invalid Marketplace version ID #{value.inspect}" unless value.to_s.match?(UUID)
203
+
204
+ value.to_s
205
+ end
206
+
207
+ def mendix_version_filter(value)
208
+ return if value.nil?
209
+
210
+ valid = value.to_s.match?(MENDIX_VERSION) || value.to_s.match?(MENDIX_BUILD_VERSION)
211
+ raise MarketplaceError, "invalid Mendix version #{value.inspect}" unless valid
212
+
213
+ value.to_s.split('.').first(3).join('.')
214
+ end
215
+
216
+ def bounded_integer(value, range)
217
+ integer = Integer(value)
218
+ raise MarketplaceError, "value #{value.inspect} is outside #{range}" unless range.cover?(integer)
219
+
220
+ integer
221
+ rescue ArgumentError, TypeError
222
+ raise MarketplaceError, "expected an integer, got #{value.inspect}"
223
+ end
224
+
225
+ def positive_integer(value, label)
226
+ integer = bounded_integer(value, 1..)
227
+ integer.to_s
228
+ rescue MarketplaceError
229
+ raise MarketplaceError, "invalid #{label} #{value.inspect}"
230
+ end
231
+
232
+ def version_not_found_message(version, mendix_version)
233
+ qualifier = if version
234
+ "version #{version.inspect}"
235
+ elsif mendix_version
236
+ "a version compatible with Mendix #{mendix_version}"
237
+ else
238
+ 'a published version'
239
+ end
240
+ "Marketplace content does not have #{qualifier}"
241
+ end
242
+
243
+ def missing_pat_message
244
+ 'Mendix PAT not found; run `mxrb marketplace login --help` and configure a PAT file ' \
245
+ 'or Ruby .env with scope mx:marketplace-content:read'
246
+ end
247
+ end # rubocop:enable Metrics/ClassLength
248
+
249
+ AuditResult = Data.define(
250
+ :name, :installed_version, :latest_version, :version_type, :issues, :outdated, :valid
251
+ )
252
+
253
+ # Reviews Content API-backed lock entries for updates and known vulnerabilities.
254
+ class SecurityAuditor
255
+ def initialize(target:, api: nil)
256
+ @target = File.expand_path(target)
257
+ @api = api
258
+ end
259
+
260
+ def audit(mendix_version: nil)
261
+ entries = OfficialMarketplace.lock(@target).fetch('packages').select do |_name, entry|
262
+ entry['source'] == 'mendix' && entry['content_id']
263
+ end
264
+ return [] if entries.empty?
265
+
266
+ @api ||= ContentApi.new
267
+ entries.map do |name, entry|
268
+ audit_entry(name, entry, mendix_version)
269
+ end
270
+ end
271
+
272
+ private
273
+
274
+ def audit_entry(name, entry, mendix_version)
275
+ installed = @api.versions(entry.fetch('content_id'), version_id: entry['version_id']).first
276
+ latest = @api.resolve(entry.fetch('content_id').to_s, mendix_version:, allow_vulnerable: true)
277
+ type, issues, installed_version = installed_metadata(installed, entry)
278
+ AuditResult.new(
279
+ name, installed_version, latest.version, type, issues,
280
+ installed_version != latest.version, !installed.nil? && type != 'Vulnerable'
281
+ )
282
+ end
283
+
284
+ def installed_metadata(installed, entry)
285
+ type = installed&.fetch('versionType', nil) || 'Unknown'
286
+ issues = Array(installed&.fetch('vulnerabilities', nil)).filter_map { _1['code'] }
287
+ version = installed&.fetch('versionNumber', nil) || entry['version']
288
+ [type, issues, version]
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,329 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module OfficialMarketplace
5
+ Dependency = Data.define(:module_name, :package, :archive, :dependencies)
6
+ WidgetDependency = Data.define(:package, :archive, :widget_ids, :required_ids)
7
+
8
+ # Preview for a recursively verified dependency installation.
9
+ class DependencyPlan
10
+ attr_reader :root, :dependencies, :widget_dependencies, :blockers
11
+
12
+ def initialize(root:, dependencies:, blockers:, widget_dependencies: [], &operation)
13
+ @root = root
14
+ @dependencies = dependencies.freeze
15
+ @widget_dependencies = widget_dependencies.freeze
16
+ @blockers = blockers.freeze
17
+ @operation = operation
18
+ @applied = false
19
+ end
20
+
21
+ def safe? = blockers.empty?
22
+ def applied? = @applied
23
+
24
+ def changes
25
+ dependencies.map { "install #{_1.module_name} #{_1.package.version}" } +
26
+ widget_dependencies.map { "install widget bundle #{_1.package.name} #{_1.package.version}" }
27
+ end
28
+
29
+ def apply!
30
+ raise MarketplaceError, 'Marketplace dependency plan was already applied' if applied?
31
+ raise MarketplaceError, "Marketplace dependency plan is blocked: #{blockers.join('; ')}" unless safe?
32
+ raise MarketplaceError, 'Marketplace dependency preview expired; rerun with apply enabled' unless @operation
33
+
34
+ apply_operation
35
+ end
36
+
37
+ def apply_resolved!
38
+ raise MarketplaceError, 'Marketplace dependency plan was already applied' if applied?
39
+ raise MarketplaceError, 'Marketplace dependency preview expired; rerun with apply enabled' unless @operation
40
+
41
+ apply_operation
42
+ end
43
+
44
+ def apply_operation
45
+ @operation.call
46
+ @applied = true
47
+ self
48
+ end
49
+
50
+ def expire!
51
+ @operation = nil unless applied?
52
+ self
53
+ end
54
+ end
55
+
56
+ # Discovers dependencies from unresolved qualified references inside the
57
+ # package MPR, then verifies Content API candidates against the MPK module.
58
+ class DependencyResolver # rubocop:disable Metrics/ClassLength
59
+ PLATFORM_MODULES = %w[System].freeze
60
+ OFFICIAL_CONTENT_IDS = {
61
+ 'FeedbackModule' => 205_506,
62
+ 'DataWidgets' => 116_540,
63
+ 'Administration' => 23_513
64
+ }.freeze
65
+ DATA_WIDGET_PATTERN = /
66
+ \Acom\.mendix\.widget\.web\.
67
+ (?:datagrid|datagriddatefilter|datagriddropdownfilter|datagridnumberfilter|
68
+ datagridtextfilter|dropdownsort|gallery|selectionhelper|treenode)\.
69
+ /ix
70
+ OFFICIAL_WIDGET_CONTENT_IDS = {
71
+ DATA_WIDGET_PATTERN => 116_540,
72
+ /\Acom\.mendix\.widget\.web\.combobox\.Combobox\z/i => 219_304
73
+ }.freeze
74
+
75
+ def initialize(target:, mpr:, installer:, api:, mendix_version:)
76
+ @target = File.expand_path(target)
77
+ @mpr = File.expand_path(mpr)
78
+ @installer = installer
79
+ @api = api
80
+ @mendix_version = mendix_version
81
+ @resolved = {}
82
+ @visited = {}
83
+ @blockers = []
84
+ @project_modules = Mxrb.open(@mpr) { _1.modules.map(&:name) }
85
+ @required_widget_ids = widget_ids(@mpr)
86
+ end
87
+
88
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
89
+ def resolve(identifier, apply: false, apply_resolved: false)
90
+ lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: @installer)
91
+ root, entry = lifecycle.installed(identifier)
92
+ Dir.mktmpdir('mxrb-marketplace-dependencies-') do |temporary|
93
+ @temporary = temporary
94
+ visit(root, safe_path(entry.fetch('archive')))
95
+ dependencies = @resolved.values.freeze
96
+ widget_dependencies = resolve_widget_dependencies
97
+ plan = DependencyPlan.new(
98
+ root:, dependencies:, widget_dependencies:, blockers: @blockers.uniq
99
+ ) do
100
+ apply_dependencies(dependencies, widget_dependencies)
101
+ end
102
+ plan.apply! if apply
103
+ plan.apply_resolved! if apply_resolved
104
+ plan.expire! unless apply || apply_resolved
105
+ return plan
106
+ end
107
+ ensure
108
+ @temporary = nil
109
+ end
110
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
111
+
112
+ private
113
+
114
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
115
+ def visit(module_name, archive)
116
+ return if @visited[module_name]
117
+
118
+ @visited[module_name] = true
119
+ dependency_names(archive).each do |dependency_name|
120
+ next if PLATFORM_MODULES.include?(dependency_name) || dependency_name == module_name
121
+ next if @project_modules.include?(dependency_name) && !packages.key?(dependency_name)
122
+
123
+ dependency = installed_dependency(dependency_name) || resolve_dependency(dependency_name)
124
+ next unless dependency
125
+
126
+ visit(dependency.module_name, dependency.archive)
127
+ @resolved[dependency.module_name] ||= dependency unless installed?(dependency.module_name)
128
+ end
129
+ end
130
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
131
+
132
+ def dependency_names(archive)
133
+ Dir.mktmpdir('mxrb-marketplace-dependency-model-') do |temporary|
134
+ project = extract_project(archive, temporary)
135
+ @required_widget_ids.concat(widget_ids(project)).uniq!
136
+ Mxrb.open(project) do |model|
137
+ return model.analyze.unresolved_references.filter_map do |reference|
138
+ reference.qualified_name.to_s.split(%r{[./]}, 2).first
139
+ end.uniq.sort
140
+ end
141
+ end
142
+ end
143
+
144
+ def extract_project(archive_path, temporary)
145
+ Zip::File.open(archive_path) do |archive|
146
+ reader = ModulePackageReader.new(archive)
147
+ descriptor = reader.descriptor
148
+ reader.extract_project(descriptor, temporary)
149
+ end
150
+ rescue Zip::Error, REXML::ParseException => e
151
+ raise MarketplaceError, "invalid Mendix dependency package: #{e.message}"
152
+ end
153
+
154
+ def installed_dependency(name)
155
+ pair = packages.find { |module_name, _entry| module_name.casecmp(name).zero? }
156
+ return unless pair
157
+
158
+ module_name, entry = pair
159
+ Dependency.new(module_name, nil, safe_path(entry.fetch('archive')), dependency_names_for(entry))
160
+ end
161
+
162
+ def dependency_names_for(entry)
163
+ Array(entry['dependencies'])
164
+ end
165
+
166
+ def resolve_dependency(name) # rubocop:disable Metrics/MethodLength
167
+ failures = []
168
+ candidates(name).each do |content|
169
+ package = resolve_candidate(content)
170
+ next unless package
171
+
172
+ archive = download_candidate(package)
173
+ inventory = ModulePackageInventory.read(archive)
174
+ next unless inventory.name == name
175
+
176
+ dependencies = dependency_names(archive)
177
+ return Dependency.new(name, package, archive, dependencies)
178
+ rescue MarketplaceError => e
179
+ failures << "#{name}: candidate #{content['contentId']} failed: #{e.message}"
180
+ end
181
+ @blockers.concat(failures)
182
+ @blockers << "#{name}: no official Marketplace package matched the module identity"
183
+ nil
184
+ end
185
+
186
+ def candidates(name)
187
+ (official_candidates(name) + queries(name).flat_map { @api.search(name: _1, limit: 100) })
188
+ .select { Installer::IMPORTABLE_CONTENT_TYPES.include?(_1['type']) }
189
+ .uniq { _1['contentId'] }
190
+ end
191
+
192
+ def official_candidates(name)
193
+ pair = OFFICIAL_CONTENT_IDS.find { |module_name, _content_id| module_name.casecmp(name).zero? }
194
+ pair ? [@api.content(pair.last)] : []
195
+ end
196
+
197
+ def queries(name)
198
+ humanized = name.gsub('_', ' ').gsub(/([a-z\d])([A-Z])/, '\\1 \\2')
199
+ [humanized, name].uniq
200
+ end
201
+
202
+ def resolve_candidate(content)
203
+ @api.resolve(content.fetch('contentId'), mendix_version: @mendix_version)
204
+ end
205
+
206
+ def download_candidate(package)
207
+ destination = File.join(@temporary, "#{package.content_id}-#{package.version_id}.mpk")
208
+ return destination if File.file?(destination)
209
+
210
+ @api.download(package.version_id, destination, download_url: package.download_url)
211
+ end
212
+
213
+ def resolve_widget_dependencies # rubocop:disable Metrics/MethodLength
214
+ missing = @required_widget_ids.reject { widget_installed?(_1) }
215
+ grouped = missing.group_by { official_widget_content_id(_1) }
216
+ Array(grouped.delete(nil)).each do |widget_id|
217
+ @blockers << "#{widget_id}: no verified official Marketplace widget mapping"
218
+ end
219
+ grouped.filter_map do |content_id, required_ids|
220
+ resolve_widget_dependency(content_id, required_ids)
221
+ rescue MarketplaceError => e
222
+ @blockers << "widget content #{content_id}: #{e.message}"
223
+ nil
224
+ end
225
+ end
226
+
227
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
228
+ def resolve_widget_dependency(content_id, required_ids)
229
+ if package_installed?(content_id)
230
+ raise MarketplaceError,
231
+ "locked official package is present but does not provide #{required_ids.join(', ')}"
232
+ end
233
+
234
+ existing = @resolved.values.find { _1.package&.content_id == content_id }
235
+ package = existing&.package || @api.resolve(content_id, mendix_version: @mendix_version)
236
+ @installer.validate_official_package!(package)
237
+ archive = existing&.archive || download_candidate(package)
238
+ inventory = WidgetBundleInventory.read(archive)
239
+ missing = required_ids - inventory.widget_ids
240
+ unless missing.empty?
241
+ raise MarketplaceError,
242
+ "downloaded package does not provide #{missing.join(', ')}"
243
+ end
244
+
245
+ WidgetDependency.new(package, archive, inventory.widget_ids, required_ids.sort)
246
+ end
247
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
248
+
249
+ def official_widget_content_id(widget_id)
250
+ pair = OFFICIAL_WIDGET_CONTENT_IDS.find { |pattern, _content_id| pattern.match?(widget_id) }
251
+ pair&.last
252
+ end
253
+
254
+ def widget_installed?(widget_id)
255
+ !Mxrb::WidgetPackage.find(@target, widget_id).nil?
256
+ end
257
+
258
+ def widget_ids(mpr_path)
259
+ Mxrb.open(mpr_path) do |model|
260
+ model.all_units.flat_map do |unit|
261
+ collect_widget_ids(model.parse_bson(unit))
262
+ end.uniq.sort
263
+ end
264
+ end
265
+
266
+ def collect_widget_ids(value)
267
+ case value
268
+ when Hash
269
+ own = value['WidgetId'].to_s
270
+ nested = value.values.flat_map { collect_widget_ids(_1) }
271
+ own.empty? ? nested : [own] + nested
272
+ when Array
273
+ value.flat_map { collect_widget_ids(_1) }
274
+ else
275
+ []
276
+ end
277
+ end
278
+
279
+ def apply_dependencies(dependencies, widget_dependencies) # rubocop:disable Metrics/MethodLength
280
+ paths = rollback_paths(dependencies, widget_dependencies)
281
+ lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: @installer)
282
+ lifecycle.send(:with_rollback, paths) do
283
+ dependencies.each do |dependency|
284
+ @installer.send(:import_module, dependency.archive, dependency.package, @mpr)
285
+ end
286
+ widget_dependencies.each do |dependency|
287
+ next if package_installed?(dependency.package.content_id)
288
+
289
+ @installer.send(:install_official_archive, dependency.archive, dependency.package, @mpr)
290
+ end
291
+ end
292
+ end
293
+
294
+ def rollback_paths(dependencies, widget_dependencies)
295
+ archives = (dependencies + widget_dependencies).map(&:archive)
296
+ assets = archives.flat_map { package_asset_paths(_1) }
297
+ [
298
+ @mpr, File.join(File.dirname(@mpr), 'mprcontents'),
299
+ File.join(@target, '.mxrb', 'marketplace.lock.json'),
300
+ File.join(@target, '.mxrb', 'marketplace'),
301
+ File.join(@target, '.mxrb', 'marketplace-originals'),
302
+ File.join(@target, 'theme', 'web', 'custom-variables.scss')
303
+ ] + assets.map { safe_path(_1) }
304
+ end
305
+
306
+ def package_asset_paths(archive)
307
+ return [File.join('widgets', WidgetPackageInventory.read(archive).project_filename)] \
308
+ if PackageEnvelope.kind(archive) == :widget
309
+
310
+ ModulePackageInventory.read(archive).files.keys
311
+ end
312
+
313
+ def package_installed?(content_id)
314
+ packages.any? { |_name, entry| entry['content_id'].to_s == content_id.to_s }
315
+ end
316
+
317
+ def packages = OfficialMarketplace.lock(@target).fetch('packages')
318
+ def installed?(name) = packages.key?(name)
319
+
320
+ def safe_path(relative)
321
+ path = File.expand_path(File.join(@target, relative.to_s))
322
+ prefix = "#{@target}#{File::SEPARATOR}"
323
+ raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix)
324
+
325
+ path
326
+ end
327
+ end
328
+ end
329
+ end