mxrb 0.1.2 → 0.1.4

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +458 -10
  3. data/docs/de-DE/README.md +1 -0
  4. data/docs/de-DE/oql-sql.md +42 -0
  5. data/docs/de-DE/project-structure.md +201 -2
  6. data/docs/de-DE/runtime-ruby.md +104 -0
  7. data/docs/de-DE/runtime-schema-migrations.md +44 -0
  8. data/docs/de-DE/validation-matrix.md +42 -4
  9. data/docs/en-US/README.md +1 -0
  10. data/docs/en-US/oql-sql.md +41 -0
  11. data/docs/en-US/project-structure.md +192 -2
  12. data/docs/en-US/runtime-ruby.md +100 -0
  13. data/docs/en-US/runtime-schema-migrations.md +41 -0
  14. data/docs/en-US/validation-matrix.md +39 -4
  15. data/docs/pt-BR/README.md +1 -0
  16. data/docs/pt-BR/entity-dsl.md +8 -0
  17. data/docs/pt-BR/oql-sql.md +41 -0
  18. data/docs/pt-BR/project-structure.md +197 -2
  19. data/docs/pt-BR/runtime-ruby.md +108 -0
  20. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  21. data/docs/pt-BR/validation-matrix.md +42 -5
  22. data/docs/pt-BR/writing.md +28 -0
  23. data/lib/mxrb/cli/help.rb +426 -0
  24. data/lib/mxrb/cli/release.rb +137 -0
  25. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  26. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  27. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  28. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  29. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  30. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  31. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  32. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  33. data/lib/mxrb/compiler/packager.rb +27 -13
  34. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  35. data/lib/mxrb/compiler/portable_packager.rb +16 -11
  36. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  37. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  38. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  39. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  40. data/lib/mxrb/domain_diagram.rb +409 -0
  41. data/lib/mxrb/dsl/builder.rb +62 -11
  42. data/lib/mxrb/environment.rb +226 -0
  43. data/lib/mxrb/exporter.rb +265 -45
  44. data/lib/mxrb/http/server.rb +107 -0
  45. data/lib/mxrb/initializer.rb +80 -9
  46. data/lib/mxrb/integrity/validator.rb +32 -7
  47. data/lib/mxrb/io/bson_codec.rb +42 -1
  48. data/lib/mxrb/io/mpr_file.rb +146 -3
  49. data/lib/mxrb/model/attribute.rb +3 -0
  50. data/lib/mxrb/model/entity.rb +44 -2
  51. data/lib/mxrb/model/microflow.rb +3 -1
  52. data/lib/mxrb/model/module.rb +71 -0
  53. data/lib/mxrb/model/page.rb +126 -15
  54. data/lib/mxrb/model/project.rb +3 -0
  55. data/lib/mxrb/modeler/catalog.rb +178 -0
  56. data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
  57. data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
  58. data/lib/mxrb/official_marketplace.rb +9 -3
  59. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  60. data/lib/mxrb/oql/server.rb +4 -8
  61. data/lib/mxrb/oql.rb +32 -4
  62. data/lib/mxrb/progress.rb +242 -0
  63. data/lib/mxrb/project_lifecycle.rb +5 -2
  64. data/lib/mxrb/ruby_app/exporter.rb +1749 -0
  65. data/lib/mxrb/ruby_app/preset.rb +506 -0
  66. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  67. data/lib/mxrb/ruby_app.rb +1361 -0
  68. data/lib/mxrb/runtime/access_control.rb +408 -0
  69. data/lib/mxrb/runtime/native.rb +1015 -93
  70. data/lib/mxrb/runtime/scheduler.rb +396 -0
  71. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  72. data/lib/mxrb/runtime/shared_store.rb +335 -0
  73. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  74. data/lib/mxrb/scaffold/generator.rb +11 -3
  75. data/lib/mxrb/team_server.rb +10 -7
  76. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  77. data/lib/mxrb/uml/class_diagram.rb +118 -0
  78. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  79. data/lib/mxrb/uml/server.rb +146 -0
  80. data/lib/mxrb/uml/support.rb +35 -0
  81. data/lib/mxrb/version.rb +1 -1
  82. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  83. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  84. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  85. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  86. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  87. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  88. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  89. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  90. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  93. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  94. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  95. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  96. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  98. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  99. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  100. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  101. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  102. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  103. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  104. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  105. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  106. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  107. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  108. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  109. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  110. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  111. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  112. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  113. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  114. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  115. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  116. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  117. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  118. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  119. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  120. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  121. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  122. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  123. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  124. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  125. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  126. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  127. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  128. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  129. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  130. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  131. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  132. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  133. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  134. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  135. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  136. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  137. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  138. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  139. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  140. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  141. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  142. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  143. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  144. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  145. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  146. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  147. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  148. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  149. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  150. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  151. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  152. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  153. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  154. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  155. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  156. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  157. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  158. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  159. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  160. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  161. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  162. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  163. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  164. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  165. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  166. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  167. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  168. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  169. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  170. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  171. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  172. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  173. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  174. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  175. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  176. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  177. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  178. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  179. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  180. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  181. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  182. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  183. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  184. data/lib/mxrb/web_ui/domain.html +15 -0
  185. data/lib/mxrb/web_ui/modeler.html +16 -0
  186. data/lib/mxrb/web_ui/uml.html +31 -0
  187. data/lib/mxrb/web_ui.rb +89 -0
  188. data/lib/mxrb/widget_certification.rb +208 -0
  189. data/lib/mxrb/writer.rb +278 -41
  190. data/lib/mxrb.rb +38 -8
  191. metadata +157 -5
@@ -7,6 +7,40 @@ module Mxrb
7
7
  # $Type: Projects$Module
8
8
  # ContainmentName: "Modules"
9
9
  class Module < Unit
10
+ INFRASTRUCTURE_DOCUMENT_ROUTES = {
11
+ 'ExportMappings$ExportMapping' => 'mappings/exports',
12
+ 'ImportMappings$ImportMapping' => 'mappings/imports',
13
+ 'JsonStructures$JsonStructure' => 'mappings/json_structures',
14
+ 'MessageDefinitions$MessageDefinitionCollection' => 'mappings/message_definitions',
15
+ 'MessageDefinitions$MessageDefinition2' => 'mappings/message_definitions',
16
+ 'XmlSchemas$XmlSchema' => 'mappings/xml_schemas',
17
+ 'Rest$PublishedRestService' => 'endpoints',
18
+ 'WebServices$PublishedService' => 'endpoints',
19
+ 'WebServices$PublishedWebService' => 'endpoints',
20
+ 'ODataPublish$PublishedODataService' => 'endpoints',
21
+ 'ODataPublish$PublishedODataService2' => 'endpoints',
22
+ 'Rest$ConsumedRestService' => 'integrations',
23
+ 'Rest$ConsumedODataService' => 'integrations',
24
+ 'AppServices$ConsumedAppService' => 'integrations',
25
+ 'ODataImport$ConsumedODataService' => 'integrations'
26
+ }.freeze
27
+ MAPPING_DOCUMENT_TYPES = INFRASTRUCTURE_DOCUMENT_ROUTES.keys.grep(
28
+ /Mappings|JsonStructures|MessageDefinitions|XmlSchemas/
29
+ ).freeze
30
+ APPLICATION_DOCUMENT_ROUTES = {
31
+ 'DataSets$DataSet' => 'queries/datasets',
32
+ 'ScheduledEvents$ScheduledEvent' => 'jobs/scheduled_events'
33
+ }.freeze
34
+ DOMAIN_DOCUMENT_ROUTES = {
35
+ 'DomainModels$ViewEntitySourceDocument' => 'oql_views',
36
+ 'Enumerations$Enumeration' => 'enumerations',
37
+ 'Constants$Constant' => 'constants'
38
+ }.freeze
39
+ EDITABLE_DOCUMENT_TYPES = (
40
+ INFRASTRUCTURE_DOCUMENT_ROUTES.keys + APPLICATION_DOCUMENT_ROUTES.keys +
41
+ DOMAIN_DOCUMENT_ROUTES.keys
42
+ ).freeze
43
+
10
44
  attr_reader :name, :sort_index, :from_app_store,
11
45
  :app_store_guid, :app_store_version, :export_level
12
46
 
@@ -84,6 +118,28 @@ module Mxrb
84
118
  .map { unit_to_doc(_1) }
85
119
  end
86
120
 
121
+ def mapping_documents
122
+ infrastructure_documents.select { MAPPING_DOCUMENT_TYPES.include?(_1[:type]) }
123
+ end
124
+
125
+ def infrastructure_documents
126
+ @infrastructure_documents ||= routed_documents(INFRASTRUCTURE_DOCUMENT_ROUTES)
127
+ end
128
+
129
+ def application_documents
130
+ @application_documents ||= routed_documents(APPLICATION_DOCUMENT_ROUTES)
131
+ end
132
+
133
+ def domain_documents
134
+ @domain_documents ||= routed_documents(DOMAIN_DOCUMENT_ROUTES)
135
+ end
136
+
137
+ def oql_view_documents
138
+ @oql_view_documents ||= domain_documents.select do |document|
139
+ document[:type] == 'DomainModels$ViewEntitySourceDocument'
140
+ end
141
+ end
142
+
87
143
  def module_roles
88
144
  @module_roles ||= begin
89
145
  raw = @mpr.children_of(@id).find { _1["ContainmentName"] == "ModuleSecurity" }
@@ -109,6 +165,21 @@ module Mxrb
109
165
  @mpr.parse_contents(unit_hash[:raw])
110
166
  end
111
167
 
168
+ def routed_documents(routes)
169
+ document_units.filter_map do |unit|
170
+ route = routes[unit[:type]]
171
+ next unless route
172
+
173
+ raw = unit.fetch(:raw)
174
+ doc = @mpr.parse_contents(raw)
175
+ {
176
+ id: raw.fetch("UnitID"), container_id: raw.fetch("ContainerID"),
177
+ containment: raw.fetch("ContainmentName"), type: unit.fetch(:type),
178
+ name: doc["Name"] || doc["name"] || raw.fetch("UnitID"), doc:, route:
179
+ }
180
+ end
181
+ end
182
+
112
183
  # Documents live in ContainmentName = "Documents" recursively under this module.
113
184
  # We do a simple two-pass: direct Documents children + Documents inside Folders.
114
185
  def document_units
@@ -10,7 +10,8 @@ module Mxrb
10
10
  attr_reader :name, :documentation, :url, :layout_id, :title,
11
11
  :popup_width, :popup_height, :popup_resizable, :excluded,
12
12
  :allowed_module_roles, :parameters, :export_level,
13
- :widgets, :data_source, :raw_document
13
+ :widgets, :data_source, :appearance_class, :appearance_style,
14
+ :raw_document
14
15
 
15
16
  def decode(doc)
16
17
  @raw_document = doc
@@ -25,6 +26,8 @@ module Mxrb
25
26
  @popup_resizable = doc["PopupResizable"] == true
26
27
  @excluded = doc["Excluded"] == true
27
28
  @export_level = doc["ExportLevel"] || "Hidden"
29
+ @appearance_class = doc.dig("Appearance", "Class").to_s
30
+ @appearance_style = doc.dig("Appearance", "Style").to_s
28
31
  @allowed_module_roles = parse_array(doc["AllowedModuleRoles"] || doc["AllowedRoles"] || doc["allowedModuleRoles"])
29
32
  @parameters = parse_array(doc["Parameters"] || doc["parameters"])
30
33
  @widgets = []
@@ -112,7 +115,8 @@ module Mxrb
112
115
  children = []
113
116
  parse_widgets(parse_array(widget["Widgets"]), children)
114
117
  target << { type: :container, name: widget["Name"] || "container",
115
- options: container_options(widget), children: children, events: [] }
118
+ options: container_options(widget), children: children,
119
+ events: container_events(widget) }
116
120
  next
117
121
  end
118
122
  if layout_container?(widget)
@@ -171,17 +175,42 @@ module Mxrb
171
175
  end
172
176
 
173
177
  def widget_options(widget, widget_type)
174
- options = {
178
+ options = appearance_options(widget).merge(
175
179
  attribute: attribute_path(widget),
176
180
  caption: widget_caption(widget, widget_type)
177
- }.compact
181
+ ).compact
182
+ parameters = template_parameters(widget)
183
+ options[:parameters] = parameters unless parameters.empty?
178
184
  options[:lines] = widget["NumberOfLines"] if widget_type == :text_area && widget["NumberOfLines"]
179
185
  options
180
186
  end
181
187
 
182
188
  def container_options(widget)
189
+ appearance_options(widget)
190
+ end
191
+
192
+ def appearance_options(widget)
183
193
  class_name = widget.dig("Appearance", "Class") || widget["Class"]
184
- class_name.to_s.empty? ? {} : { class: class_name }
194
+ style = widget.dig("Appearance", "Style") || widget["Style"]
195
+ dynamic = widget.dig("Appearance", "DynamicClasses")
196
+ visibility = widget.dig("ConditionalVisibilitySettings", "Expression")
197
+ { class: class_name.to_s, style: style.to_s,
198
+ dynamic_class: dynamic.to_s, visible: visibility.to_s }.reject { |_key, value| value.empty? }
199
+ end
200
+
201
+ def container_events(widget)
202
+ action = parse_action(widget["OnClickAction"] || widget["Action"])
203
+ action ? [action.merge(event: :on_click)] : []
204
+ end
205
+
206
+ def template_parameters(widget)
207
+ template = widget["Content"] || widget["CaptionTemplate"] || widget["LabelTemplate"]
208
+ parse_array(template.is_a?(Hash) ? template["Parameters"] : nil).filter_map do |parameter|
209
+ expression = parameter["Expression"].to_s
210
+ attribute = parameter.dig("AttributeRef", "Attribute").to_s.split('.').last.to_s
211
+ expression = "$currentObject/#{attribute}" if expression.empty? && !attribute.empty?
212
+ expression unless expression.empty?
213
+ end
185
214
  end
186
215
 
187
216
  def attribute_path(widget)
@@ -210,27 +239,58 @@ module Mxrb
210
239
  options = { entity: grid_entity(widget), columns: columns }.compact
211
240
  options[:search_bar] = parse_search_bar(widget["SearchBar"]) if widget["SearchBar"].is_a?(Hash)
212
241
  options[:toolbar] = parse_toolbar(widget["ToolBar"]) if widget["ToolBar"].is_a?(Hash)
213
- { type: :data_grid, name: widget["Name"], options: options, events: [] }
242
+ { type: :data_grid, name: widget["Name"], options: options, events: grid_events(widget) }
214
243
  end
215
244
 
216
245
  def pluggable_widget(widget)
217
246
  case widget.dig("Type", "WidgetId")
218
247
  when "com.mendix.widget.web.datagrid.Datagrid" then data_grid2_widget(widget)
219
248
  when "com.mendix.widget.web.combobox.Combobox" then combo_box_widget(widget)
249
+ when "com.mendix.widget.web.gallery.Gallery" then gallery_widget(widget)
220
250
  else native_widget(widget)
221
251
  end
222
252
  end
223
253
 
254
+ def gallery_widget(widget)
255
+ properties = custom_property_map(widget["Object"], widget.dig("Type", "ObjectType"))
256
+ source = properties.dig("datasource", "DataSource") || {}
257
+ children = []
258
+ parse_widgets(parse_array(properties.dig("content", "Widgets")), children)
259
+ sort = parse_array(source.dig("SortBar", "SortItems")).map do |item|
260
+ {
261
+ attribute: item.dig("AttributeRef", "Attribute"),
262
+ direction: item["SortDirection"].to_s
263
+ }.compact
264
+ end
265
+ xpath = source["XPathConstraint"].to_s
266
+ options = appearance_options(widget).merge(
267
+ entity: source.dig("EntityRef", "Entity"), xpath: xpath,
268
+ association: xpath[/\[([\w.]+)\s*=\s*\$\w+\]/, 1],
269
+ context_variable: xpath[/\$([A-Za-z_]\w*)/, 1], sort: sort
270
+ ).compact
271
+ {
272
+ type: :gallery, name: widget["Name"], options: options,
273
+ children: children, events: []
274
+ }
275
+ end
276
+
224
277
  def native_widget(widget)
225
278
  deep = widget.reject { |key, _| %w[$ID $Type Name].include?(key.to_s) }
279
+ widget_id = widget.dig("Type", "WidgetId").to_s
280
+ platform = widget.dig("Type", "SupportedPlatform").to_s
281
+ options = { native_type: widget["$Type"], deep_structure: deep }
282
+ options[:widget_id] = widget_id unless widget_id.empty?
283
+ options[:platform] = platform unless platform.empty?
226
284
  {
227
285
  type: :native_widget, name: widget["Name"] || "widget",
228
- options: { native_type: widget["$Type"], deep_structure: deep }, events: []
286
+ options:, events: []
229
287
  }
230
288
  end
231
289
 
232
290
  def data_grid2_widget(widget)
233
291
  properties = custom_property_map(widget["Object"], widget.dig("Type", "ObjectType"))
292
+ return data_grid_widget(widget) if properties.empty? && widget["DataSource"]
293
+
234
294
  entity = properties.dig("datasource", "DataSource", "EntityRef", "Entity")
235
295
  column_type = custom_property_type(widget.dig("Type", "ObjectType"), "columns")
236
296
  &.dig("ValueType", "ObjectType")
@@ -244,12 +304,35 @@ module Mxrb
244
304
  end
245
305
  {
246
306
  type: :data_grid, name: widget["Name"],
247
- options: { entity: entity, columns: columns }.compact, events: []
307
+ options: { entity: entity, columns: columns }.compact, events: grid_events(widget)
248
308
  }
249
309
  end
250
310
 
311
+ def grid_events(widget)
312
+ {
313
+ "OnChangeAction" => :on_change,
314
+ "OnClickAction" => :on_click,
315
+ "Action" => :on_click
316
+ }.filter_map do |property, event|
317
+ action = parse_action(widget[property])
318
+ action&.merge(event: event)
319
+ end
320
+ end
321
+
251
322
  def combo_box_widget(widget)
252
323
  properties = custom_property_map(widget["Object"], widget.dig("Type", "ObjectType"))
324
+ if properties.empty? && widget["AttributePath"]
325
+ reference = widget["SelectorType"] == "Reference"
326
+ options = {
327
+ attribute: widget["AttributePath"], caption: extract_text(widget["LabelText"]),
328
+ display_attribute: widget["DisplayAttribute"]
329
+ }.compact
330
+ return {
331
+ type: reference ? :reference_selector : :drop_down,
332
+ name: widget["Name"], options: options, events: grid_events(widget)
333
+ }
334
+ end
335
+
253
336
  association = parse_array(properties.dig("attributeAssociation", "EntityRef", "Steps"))
254
337
  .first&.fetch("Association", nil)
255
338
  reference = properties.dig("optionsSourceType", "PrimitiveValue") == "association"
@@ -267,7 +350,7 @@ module Mxrb
267
350
  )
268
351
  end
269
352
  { type: reference ? :reference_selector : :drop_down,
270
- name: widget["Name"], options: options.compact, events: [] }
353
+ name: widget["Name"], options: options.compact, events: grid_events(widget) }
271
354
  end
272
355
 
273
356
  def custom_property_map(object, object_type)
@@ -376,21 +459,49 @@ module Mxrb
376
459
  def parse_source(source)
377
460
  return nil unless source.is_a?(Hash)
378
461
  result = if %w[Pages$NanoflowSource Forms$NanoflowSource].include?(source["$Type"])
379
- { kind: :nanoflow, name: local_name(source["Nanoflow"] || source.dig("NanoflowSettings", "Nanoflow")) }
462
+ { kind: :nanoflow, name: source_name(source["Nanoflow"] || source.dig("NanoflowSettings", "Nanoflow")) }
380
463
  else
381
- { kind: :microflow, name: local_name(source["Microflow"] || source.dig("MicroflowSettings", "Microflow")) }
464
+ { kind: :microflow, name: source_name(source["Microflow"] || source.dig("MicroflowSettings", "Microflow")) }
382
465
  end
383
466
  result[:name].to_s.empty? ? nil : result
384
467
  end
385
468
 
469
+ # Page data sources are architectural references. Unlike widget-local
470
+ # action handlers, their module qualification must survive export so a
471
+ # native Core.Flow reference cannot silently become Portal.Flow.
472
+ def source_name(name)
473
+ name.to_s
474
+ end
475
+
386
476
  def parse_action(action)
387
477
  return nil unless action.is_a?(Hash)
388
478
  if %w[Pages$CallNanoflowClientAction Forms$CallNanoflowClientAction].include?(action["$Type"])
389
- handler = local_name(action["Nanoflow"] || action.dig("NanoflowSettings", "Nanoflow"))
390
- handler.to_s.empty? ? nil : { kind: :nanoflow, handler: handler }
479
+ settings = action["NanoflowSettings"] || action
480
+ handler = local_name(action["Nanoflow"] || settings["Nanoflow"])
481
+ return nil if handler.to_s.empty?
482
+
483
+ arguments = parse_array(settings["ParameterMappings"]).to_h do |mapping|
484
+ [local_name(mapping["Parameter"]), mapping["Expression"].to_s]
485
+ end
486
+ { kind: :nanoflow, handler: handler, arguments: arguments }
391
487
  elsif %w[Pages$MicroflowClientAction Forms$MicroflowAction Forms$MicroflowClientAction].include?(action["$Type"])
392
- handler = local_name(action["Microflow"] || action.dig("MicroflowSettings", "Microflow"))
393
- handler.to_s.empty? ? nil : { kind: :microflow, handler: handler }
488
+ settings = action["MicroflowSettings"] || action
489
+ handler = local_name(action["Microflow"] || settings["Microflow"])
490
+ return nil if handler.to_s.empty?
491
+
492
+ arguments = parse_array(settings["ParameterMappings"]).to_h do |mapping|
493
+ [local_name(mapping["Parameter"]), mapping["Expression"].to_s]
494
+ end
495
+ { kind: :microflow, handler: handler, arguments: arguments }
496
+ elsif %w[Pages$FormAction Forms$FormAction].include?(action["$Type"])
497
+ settings = action["FormSettings"] || action
498
+ handler = settings["Form"].to_s
499
+ return nil if handler.empty?
500
+
501
+ arguments = parse_array(settings["ParameterMappings"]).to_h do |mapping|
502
+ [local_name(mapping["Parameter"]), mapping["Expression"].to_s]
503
+ end
504
+ { kind: :page, handler: handler, arguments: arguments }
394
505
  elsif %w[Pages$SaveChangesClientAction Forms$SaveChangesClientAction].include?(action["$Type"])
395
506
  { kind: :action, handler: "save_changes" }
396
507
  elsif %w[Pages$CancelChangesClientAction Forms$CancelChangesClientAction].include?(action["$Type"])
@@ -63,6 +63,9 @@ module Mxrb
63
63
  analyzer = Oql::Analyzer.new(dialect:)
64
64
  oql_queries.map { analyzer.analyze(_1) }.freeze
65
65
  end
66
+ def sql_to_oql(source, dialect: :postgresql)
67
+ Oql::ReverseTranslator.new(dialect:, project: self).translate(source)
68
+ end
66
69
 
67
70
  # ── Semantic analysis ───────────────────────────────────────────────
68
71
 
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mxrb
4
+ module Modeler
5
+ # Read-only, JSON-safe projection used by the React project modeler.
6
+ # rubocop:disable Metrics
7
+ class Catalog
8
+ SETTINGS_PATTERN = /(?:Settings|Configuration)\$|(?:Settings|Configuration)\z/
9
+
10
+ def initialize(path)
11
+ @path = File.expand_path(path)
12
+ end
13
+
14
+ def to_h
15
+ project = Model::Project.open(@path)
16
+ documents = project_documents(project)
17
+ modules = project.modules.map { module_payload(_1) }
18
+ {
19
+ project: project_payload(project),
20
+ summary: summary(modules),
21
+ modules:,
22
+ navigation: project.navigation.to_h,
23
+ security: security_payload(documents),
24
+ settings: settings_payload(documents)
25
+ }
26
+ ensure
27
+ project&.close
28
+ end
29
+
30
+ private
31
+
32
+ def project_payload(project)
33
+ {
34
+ name: project.name,
35
+ mendix_version: project.mendix_version,
36
+ format_version: project.format_version
37
+ }
38
+ end
39
+
40
+ def summary(modules)
41
+ keys = %i[entities pages microflows nanoflows integrations configurations]
42
+ { modules: modules.size }.merge(
43
+ keys.to_h { |key| [key, modules.sum { Array(_1[key]).size }] }
44
+ )
45
+ end
46
+
47
+ def module_payload(mod)
48
+ documents = catalog_documents(mod)
49
+ integration_groups = %w[endpoints integrations mappings]
50
+ {
51
+ id: mod.id,
52
+ name: mod.name,
53
+ marketplace: mod.from_app_store == true,
54
+ marketplace_guid: blank_to_nil(mod.app_store_guid),
55
+ marketplace_version: blank_to_nil(mod.app_store_version),
56
+ entities: mod.entities.map { entity_payload(_1, mod.name) },
57
+ pages: mod.pages.map { page_payload(_1, mod.name) },
58
+ microflows: mod.microflows.map { flow_payload(_1, mod.name, 'microflow') },
59
+ nanoflows: mod.nanoflows.map { flow_payload(_1, mod.name, 'nanoflow') },
60
+ module_roles: mod.module_roles,
61
+ integrations: documents.select { integration_groups.include?(_1[:group]) },
62
+ configurations: documents.reject { integration_groups.include?(_1[:group]) }
63
+ }
64
+ end
65
+
66
+ def entity_payload(entity, module_name)
67
+ {
68
+ id: entity.id,
69
+ name: entity.name,
70
+ qualified_name: entity.qualified_name || "#{module_name}.#{entity.name}",
71
+ persistent: entity.persistable == true,
72
+ attributes: entity.attributes.map do |attribute|
73
+ { name: attribute.name, type: attribute.type.to_s, required: attribute.required == true }
74
+ end
75
+ }
76
+ end
77
+
78
+ def page_payload(page, module_name)
79
+ widgets = flatten_widgets(page.widgets)
80
+ {
81
+ id: page.id,
82
+ name: page.name,
83
+ qualified_name: "#{module_name}.#{page.name}",
84
+ title: page.title,
85
+ url: page.url,
86
+ layout_id: page.layout_id,
87
+ excluded: page.excluded == true,
88
+ allowed_module_roles: page.allowed_module_roles.map(&:to_s),
89
+ widget_count: widgets.size,
90
+ widget_types: widgets.filter_map { _1[:type]&.to_s }.tally
91
+ }
92
+ end
93
+
94
+ def flow_payload(flow, module_name, kind)
95
+ {
96
+ id: flow.id,
97
+ name: flow.name,
98
+ qualified_name: "#{module_name}.#{flow.name}",
99
+ kind:,
100
+ documentation: flow.documentation.to_s,
101
+ allowed_module_roles: flow.allowed_module_roles.map(&:to_s),
102
+ parameter_count: flow.parameters.size,
103
+ object_count: flow.objects.size,
104
+ flow_count: flow.flows.size
105
+ }
106
+ end
107
+
108
+ def flatten_widgets(widgets)
109
+ Array(widgets).flat_map do |widget|
110
+ next [] unless widget.is_a?(Hash)
111
+
112
+ [widget, *flatten_widgets(widget[:children] || widget['children'])]
113
+ end
114
+ end
115
+
116
+ def catalog_documents(mod)
117
+ documents = mod.infrastructure_documents + mod.application_documents + mod.domain_documents
118
+ documents.map do |document|
119
+ {
120
+ id: document[:id],
121
+ name: document[:name].to_s,
122
+ type: document[:type],
123
+ group: document[:route].to_s.split('/').first
124
+ }
125
+ end
126
+ end
127
+
128
+ def project_documents(project)
129
+ project.all_units.filter_map do |unit|
130
+ document = project.parse_bson(unit)
131
+ type = document['$Type'].to_s
132
+ next if type.empty?
133
+
134
+ { id: unit['UnitID'], type:, document: }
135
+ end
136
+ end
137
+
138
+ def security_payload(documents)
139
+ source = documents.find { _1[:type] == 'Security$ProjectSecurity' }&.fetch(:document)
140
+ return { configured: false, level: nil, user_roles: [] } unless source
141
+
142
+ roles = array(source['UserRoles']).map do |role|
143
+ {
144
+ name: role['Name'].to_s,
145
+ description: role['Description'].to_s,
146
+ module_roles: array(role['ModuleRoles']).map(&:to_s)
147
+ }
148
+ end
149
+ { configured: true, level: source['SecurityLevel'], user_roles: roles }
150
+ end
151
+
152
+ def settings_payload(documents)
153
+ documents.filter { _1[:type].match?(SETTINGS_PATTERN) }.map do |entry|
154
+ document = entry[:document]
155
+ {
156
+ id: entry[:id],
157
+ type: entry[:type],
158
+ name: document['Name'].to_s,
159
+ values: scalar_values(document)
160
+ }
161
+ end
162
+ end
163
+
164
+ def scalar_values(document)
165
+ document.each_with_object({}) do |(key, value), result|
166
+ next if key.start_with?('$') || value.is_a?(Hash) || value.is_a?(Array)
167
+
168
+ result[key] = value
169
+ end
170
+ end
171
+
172
+ def array(value) = IO::BsonCodec.parse_array(value)[:items]
173
+
174
+ def blank_to_nil(value) = value.to_s.empty? ? nil : value
175
+ end
176
+ # rubocop:enable Metrics
177
+ end
178
+ end
@@ -122,6 +122,7 @@ module Mxrb
122
122
  def install(staged_files, protected_files = [])
123
123
  staged_files.each do |relative, source|
124
124
  install_file(relative, source) unless protected_files.include?(relative)
125
+ yield(relative) if block_given?
125
126
  end
126
127
  end
127
128
 
@@ -179,9 +180,12 @@ module Mxrb
179
180
  end
180
181
 
181
182
  def import!
182
- validate_inputs!
183
- Dir.mktmpdir('mxrb-module-import-') do |temporary|
184
- Zip::File.open(@package_path) { import_archive(_1, temporary) }
183
+ Progress.with("Importing #{File.basename(@package_path)}") do |progress|
184
+ validate_inputs!
185
+ progress.update(detail: 'reading package')
186
+ Dir.mktmpdir('mxrb-module-import-') do |temporary|
187
+ Zip::File.open(@package_path) { import_archive(_1, temporary, progress) }
188
+ end
185
189
  end
186
190
  rescue Zip::Error, REXML::ParseException => e
187
191
  raise MarketplaceError, "invalid Mendix module package: #{e.message}"
@@ -189,12 +193,12 @@ module Mxrb
189
193
 
190
194
  private
191
195
 
192
- def import_archive(archive, temporary)
196
+ def import_archive(archive, temporary, progress)
193
197
  reader = ModulePackageReader.new(archive)
194
198
  descriptor = reader.descriptor
195
199
  source_path = reader.extract_project(descriptor, temporary)
196
200
  staged_files = reader.stage_files(descriptor.files, temporary)
197
- import_project(source_path, descriptor, staged_files, temporary)
201
+ import_project(source_path, descriptor, staged_files, temporary, progress)
198
202
  end
199
203
 
200
204
  def validate_inputs!
@@ -203,12 +207,13 @@ module Mxrb
203
207
  raise MarketplaceError, "target root not found: #{@target_root}" unless File.directory?(@target_root)
204
208
  end
205
209
 
206
- def import_project(source_path, descriptor, staged_files, temporary) # rubocop:disable Metrics/MethodLength
210
+ def import_project(source_path, descriptor, staged_files, temporary, progress) # rubocop:disable Metrics/MethodLength
207
211
  source = IO::MprFile.open(source_path, readonly: true)
208
212
  target = IO::MprFile.open(@mpr_path)
209
213
  assets = ModulePackageAssets.new(@target_root, temporary)
210
214
  imported_ids = []
211
- import_transaction(source, target, descriptor, staged_files, assets, imported_ids)
215
+ import_transaction(source, target, descriptor, staged_files, assets, imported_ids, progress)
216
+ progress.advance(detail: 'import transaction')
212
217
  import_result(source, target, descriptor, staged_files, imported_ids)
213
218
  rescue StandardError
214
219
  assets&.rollback
@@ -220,24 +225,30 @@ module Mxrb
220
225
  end
221
226
 
222
227
  # rubocop:disable Metrics/ParameterLists
223
- def import_transaction(source, target, descriptor, staged_files, assets, imported_ids)
228
+ def import_transaction(source, target, descriptor, staged_files, assets, imported_ids, progress) # rubocop:disable Metrics/MethodLength
224
229
  validate_versions!(source, target, descriptor)
225
230
  module_unit, units = package_units(source, descriptor.name)
226
231
  validate_target!(target, descriptor.name, units)
232
+ progress.update(
233
+ current: 2, total: units.size + staged_files.size + 3,
234
+ detail: "#{units.size} model units"
235
+ )
227
236
  target.transaction do
228
- imported_ids.concat(insert_units(source, target, module_unit, units))
229
- assets.install(staged_files, @protected_files)
237
+ imported_ids.concat(insert_units(source, target, module_unit, units, progress))
238
+ assets.install(staged_files, @protected_files) do |relative|
239
+ progress.advance(detail: "asset #{relative}")
240
+ end
230
241
  end
231
242
  end
232
243
  # rubocop:enable Metrics/ParameterLists
233
244
 
234
- def insert_units(source, target, module_unit, units)
245
+ def insert_units(source, target, module_unit, units, progress)
235
246
  units.map do |unit|
236
247
  container = unit == module_unit ? target.root_unit.fetch('UnitID') : unit.fetch('ContainerID')
237
248
  target.insert_unit(
238
249
  container_uuid: container, containment_name: unit.fetch('ContainmentName'),
239
250
  contents_doc: source.parse_contents(unit)
240
- )
251
+ ).tap { progress.advance(detail: "unit #{unit.fetch('UnitID')}") }
241
252
  end
242
253
  end
243
254
 
@@ -200,17 +200,21 @@ module Mxrb
200
200
  end
201
201
 
202
202
  def install(archive, package)
203
- validate_target!
204
- inventory = WidgetPackageInventory.read(archive)
205
- validate_identity!(inventory, package)
206
- digest = Digest::SHA256.file(archive).hexdigest
207
- destination = safe_path(File.join('widgets', inventory.project_filename))
208
- cache = safe_path(cache_relative(inventory, package))
209
- lock_path = safe_path(File.join('.mxrb', 'marketplace.lock.json'))
210
- current = validate_boundaries!(package, destination, cache, lock_path)
211
- install_transaction(
212
- archive, package, inventory, digest, destination, cache, lock_path, current
213
- )
203
+ Progress.with("Installing widget #{package.name}") do |progress|
204
+ validate_target!
205
+ progress.update(detail: 'reading widget package')
206
+ inventory = WidgetPackageInventory.read(archive)
207
+ validate_identity!(inventory, package)
208
+ digest = Digest::SHA256.file(archive).hexdigest
209
+ destination = safe_path(File.join('widgets', inventory.project_filename))
210
+ cache = safe_path(cache_relative(inventory, package))
211
+ lock_path = safe_path(File.join('.mxrb', 'marketplace.lock.json'))
212
+ current = validate_boundaries!(package, destination, cache, lock_path)
213
+ progress.update(detail: 'installing project assets')
214
+ install_transaction(
215
+ archive, package, inventory, digest, destination, cache, lock_path, current
216
+ )
217
+ end
214
218
  end
215
219
 
216
220
  private
@@ -223,14 +223,20 @@ module Mxrb
223
223
  end
224
224
 
225
225
  def json(url, authorization: default_authorization)
226
- JSON.parse(get(url, accept: 'application/json', authorization:))
226
+ Progress.with('Loading Marketplace data') do |progress|
227
+ progress.update(detail: URI.parse(url).host)
228
+ JSON.parse(get(url, accept: 'application/json', authorization:))
229
+ end
227
230
  rescue JSON::ParserError => e
228
231
  raise MarketplaceError, "invalid JSON response: #{e.message}"
229
232
  end
230
233
 
231
234
  def download(url, destination, authorization: default_authorization)
232
- File.binwrite(destination, get(url, accept: 'application/octet-stream', authorization:))
233
- destination
235
+ Progress.with("Downloading #{File.basename(destination)}") do |progress|
236
+ progress.update(detail: URI.parse(url).host)
237
+ File.binwrite(destination, get(url, accept: 'application/octet-stream', authorization:))
238
+ destination
239
+ end
234
240
  end
235
241
 
236
242
  private