mxrb 0.1.3 → 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 (181) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +455 -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/oql-sql.md +41 -0
  17. data/docs/pt-BR/project-structure.md +197 -2
  18. data/docs/pt-BR/runtime-ruby.md +108 -0
  19. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  20. data/docs/pt-BR/validation-matrix.md +42 -5
  21. data/lib/mxrb/cli/help.rb +426 -0
  22. data/lib/mxrb/cli/release.rb +137 -0
  23. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  24. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  25. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  26. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  27. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  28. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  29. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  30. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  31. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  32. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  33. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  34. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  35. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  36. data/lib/mxrb/domain_diagram.rb +409 -0
  37. data/lib/mxrb/dsl/builder.rb +43 -9
  38. data/lib/mxrb/environment.rb +226 -0
  39. data/lib/mxrb/exporter.rb +89 -15
  40. data/lib/mxrb/http/server.rb +107 -0
  41. data/lib/mxrb/initializer.rb +80 -9
  42. data/lib/mxrb/integrity/validator.rb +14 -0
  43. data/lib/mxrb/io/bson_codec.rb +42 -1
  44. data/lib/mxrb/io/mpr_file.rb +146 -3
  45. data/lib/mxrb/model/attribute.rb +3 -0
  46. data/lib/mxrb/model/entity.rb +28 -2
  47. data/lib/mxrb/model/microflow.rb +3 -1
  48. data/lib/mxrb/model/page.rb +126 -15
  49. data/lib/mxrb/model/project.rb +3 -0
  50. data/lib/mxrb/modeler/catalog.rb +178 -0
  51. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  52. data/lib/mxrb/oql/server.rb +4 -8
  53. data/lib/mxrb/oql.rb +23 -1
  54. data/lib/mxrb/project_lifecycle.rb +5 -2
  55. data/lib/mxrb/ruby_app/exporter.rb +1749 -0
  56. data/lib/mxrb/ruby_app/preset.rb +506 -0
  57. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  58. data/lib/mxrb/ruby_app.rb +1361 -0
  59. data/lib/mxrb/runtime/access_control.rb +408 -0
  60. data/lib/mxrb/runtime/native.rb +1015 -93
  61. data/lib/mxrb/runtime/scheduler.rb +396 -0
  62. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  63. data/lib/mxrb/runtime/shared_store.rb +335 -0
  64. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  65. data/lib/mxrb/scaffold/generator.rb +11 -3
  66. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  67. data/lib/mxrb/uml/class_diagram.rb +118 -0
  68. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  69. data/lib/mxrb/uml/server.rb +146 -0
  70. data/lib/mxrb/uml/support.rb +35 -0
  71. data/lib/mxrb/version.rb +1 -1
  72. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  73. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  74. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  75. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  76. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  77. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  78. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  79. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  80. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  81. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  82. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  83. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  84. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  85. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  86. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  87. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  88. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  89. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  90. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  93. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  94. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  95. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  96. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  98. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  99. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  100. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  101. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  102. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  103. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  104. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  105. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  106. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  107. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  108. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  109. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  110. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  111. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  112. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  113. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  114. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  115. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  116. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  117. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  118. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  119. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  120. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  121. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  122. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  123. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  124. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  125. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  126. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  127. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  128. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  129. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  130. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  131. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  132. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  133. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  134. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  135. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  136. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  137. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  138. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  139. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  140. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  141. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  142. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  143. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  144. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  145. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  146. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  147. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  148. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  149. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  150. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  151. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  152. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  153. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  154. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  155. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  156. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  157. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  158. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  159. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  160. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  161. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  162. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  163. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  164. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  165. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  166. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  167. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  168. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  169. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  170. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  171. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  172. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  173. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  174. data/lib/mxrb/web_ui/domain.html +15 -0
  175. data/lib/mxrb/web_ui/modeler.html +16 -0
  176. data/lib/mxrb/web_ui/uml.html +31 -0
  177. data/lib/mxrb/web_ui.rb +89 -0
  178. data/lib/mxrb/widget_certification.rb +208 -0
  179. data/lib/mxrb/writer.rb +200 -24
  180. data/lib/mxrb.rb +26 -1
  181. metadata +156 -5
@@ -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