mxrb 0.1.3 → 0.1.5

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 +257 -17
  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 +2508 -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 +1369 -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,187 @@ 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
- case widget.dig("Type", "WidgetId")
246
+ widget_id = widget.dig("Type", "WidgetId").to_s
247
+ return native_widget(widget) if widget_id.empty?
248
+
249
+ case widget_id
218
250
  when "com.mendix.widget.web.datagrid.Datagrid" then data_grid2_widget(widget)
219
251
  when "com.mendix.widget.web.combobox.Combobox" then combo_box_widget(widget)
220
- else native_widget(widget)
252
+ when "com.mendix.widget.web.gallery.Gallery" then gallery_widget(widget)
253
+ else generic_pluggable_widget(widget)
254
+ end
255
+ end
256
+
257
+ def generic_pluggable_widget(widget)
258
+ object_type = widget.dig("Type", "ObjectType")
259
+ properties = pluggable_properties(widget["Object"], object_type)
260
+ children = nested_pluggable_widgets(properties)
261
+ properties = strip_pluggable_widgets(properties)
262
+ options = appearance_options(widget).merge(
263
+ widget_id: widget.dig("Type", "WidgetId"),
264
+ widget_name: widget.dig("Type", "WidgetName"),
265
+ platform: widget.dig("Type", "SupportedPlatform"),
266
+ properties:
267
+ ).compact
268
+ {
269
+ type: :pluggable_widget, name: widget["Name"] || "widget",
270
+ options:, children:, events: pluggable_events(properties)
271
+ }
272
+ end
273
+
274
+ def pluggable_properties(object, object_type)
275
+ return {} unless object.is_a?(Hash) && object_type.is_a?(Hash)
276
+
277
+ types = parse_array(object_type["PropertyTypes"])
278
+ types_by_id = types.to_h { [IO::BsonCodec.extract_id(_1["$ID"]), _1] }
279
+ parse_array(object["Properties"]).each_with_object({}) do |property, result|
280
+ type = types_by_id[IO::BsonCodec.extract_id(property["TypePointer"])]
281
+ next unless type
282
+
283
+ result[type["PropertyKey"].to_s] = pluggable_value(
284
+ property["Value"], type["ValueType"]
285
+ )
286
+ end
287
+ end
288
+
289
+ def pluggable_value(value, value_type)
290
+ return nil unless value.is_a?(Hash)
291
+
292
+ type = value_type.is_a?(Hash) ? value_type["Type"].to_s : ""
293
+ widgets = parse_array(value["Widgets"])
294
+ unless widgets.empty?
295
+ parsed = []
296
+ parse_widgets(widgets, parsed)
297
+ return { widgets: parsed }
298
+ end
299
+ objects = parse_array(value["Objects"])
300
+ unless objects.empty?
301
+ object_type = value_type.is_a?(Hash) ? value_type["ObjectType"] : nil
302
+ return { objects: objects.map { pluggable_properties(_1, object_type) } }
303
+ end
304
+
305
+ return extract_text(value["TextTemplate"]) if value["TextTemplate"].is_a?(Hash)
306
+ return value.dig("AttributeRef", "Attribute") if value["AttributeRef"].is_a?(Hash)
307
+ return pluggable_data_source(value["DataSource"]) if value["DataSource"].is_a?(Hash)
308
+ return value["Expression"] unless value["Expression"].to_s.empty?
309
+ return value["Image"] unless value["Image"].to_s.empty?
310
+ return value["Form"] unless value["Form"].to_s.empty?
311
+
312
+ primitive = value["PrimitiveValue"]
313
+ return primitive == true || primitive.to_s.casecmp("true").zero? if type == "Boolean"
314
+ return primitive.to_i if type == "Integer"
315
+ return primitive.to_f if %w[Decimal Number].include?(type)
316
+ return primitive unless primitive.nil? || primitive.to_s.empty?
317
+
318
+ action = value["Action"]
319
+ return pluggable_action(action) if action.is_a?(Hash) && action["$Type"] != "Forms$NoAction"
320
+ return value["Selection"] unless value["Selection"].to_s.empty? || value["Selection"] == "None"
321
+
322
+ nil
323
+ end
324
+
325
+ def pluggable_data_source(source)
326
+ {
327
+ data_source: source.dig("EntityRef", "Entity") || source["Entity"],
328
+ xpath: source["XPathConstraint"].to_s,
329
+ sort: parse_array(source.dig("SortBar", "SortItems")).map do |item|
330
+ {
331
+ attribute: item.dig("AttributeRef", "Attribute"),
332
+ direction: item["SortDirection"].to_s
333
+ }.compact
334
+ end
335
+ }.compact
336
+ end
337
+
338
+ def pluggable_action(action)
339
+ candidates = {
340
+ "nanoflow" => action["Nanoflow"],
341
+ "microflow" => action["Microflow"],
342
+ "page" => action["Form"]
343
+ }
344
+ kind, handler = candidates.find { |_candidate, value| !value.to_s.empty? }
345
+ kind ||= "action"
346
+ handler ||= action["$Type"]
347
+ { kind:, handler: handler.to_s }
348
+ end
349
+
350
+ def nested_pluggable_widgets(value, found = [])
351
+ case value
352
+ when Hash
353
+ widgets = value[:widgets] || value["widgets"]
354
+ found.concat(Array(widgets)) if widgets
355
+ value.each_value { nested_pluggable_widgets(_1, found) }
356
+ when Array then value.each { nested_pluggable_widgets(_1, found) }
357
+ end
358
+ found.uniq { [_1[:name] || _1["name"], _1[:type] || _1["type"]] }
359
+ end
360
+
361
+ def strip_pluggable_widgets(value)
362
+ case value
363
+ when Hash
364
+ value.each_with_object({}) do |(key, child), result|
365
+ next if key.to_s == "widgets"
366
+
367
+ result[key] = strip_pluggable_widgets(child)
368
+ end
369
+ when Array then value.map { strip_pluggable_widgets(_1) }
370
+ else value
371
+ end
372
+ end
373
+
374
+ def pluggable_events(properties)
375
+ properties.filter_map do |key, value|
376
+ next unless value.is_a?(Hash) && value[:handler]
377
+
378
+ event = key.to_s.match?(/change/i) ? :on_change : :on_click
379
+ value.merge(event:)
380
+ end
381
+ end
382
+
383
+ def gallery_widget(widget)
384
+ properties = custom_property_map(widget["Object"], widget.dig("Type", "ObjectType"))
385
+ source = properties.dig("datasource", "DataSource") || {}
386
+ children = []
387
+ parse_widgets(parse_array(properties.dig("content", "Widgets")), children)
388
+ sort = parse_array(source.dig("SortBar", "SortItems")).map do |item|
389
+ {
390
+ attribute: item.dig("AttributeRef", "Attribute"),
391
+ direction: item["SortDirection"].to_s
392
+ }.compact
221
393
  end
394
+ xpath = source["XPathConstraint"].to_s
395
+ options = appearance_options(widget).merge(
396
+ entity: source.dig("EntityRef", "Entity"), xpath: xpath,
397
+ association: xpath[/\[([\w.]+)\s*=\s*\$\w+\]/, 1],
398
+ context_variable: xpath[/\$([A-Za-z_]\w*)/, 1], sort: sort
399
+ ).compact
400
+ {
401
+ type: :gallery, name: widget["Name"], options: options,
402
+ children: children, events: []
403
+ }
222
404
  end
223
405
 
224
406
  def native_widget(widget)
225
407
  deep = widget.reject { |key, _| %w[$ID $Type Name].include?(key.to_s) }
408
+ widget_id = widget.dig("Type", "WidgetId").to_s
409
+ platform = widget.dig("Type", "SupportedPlatform").to_s
410
+ options = { native_type: widget["$Type"], deep_structure: deep }
411
+ options[:widget_id] = widget_id unless widget_id.empty?
412
+ options[:platform] = platform unless platform.empty?
226
413
  {
227
414
  type: :native_widget, name: widget["Name"] || "widget",
228
- options: { native_type: widget["$Type"], deep_structure: deep }, events: []
415
+ options:, events: []
229
416
  }
230
417
  end
231
418
 
232
419
  def data_grid2_widget(widget)
233
420
  properties = custom_property_map(widget["Object"], widget.dig("Type", "ObjectType"))
421
+ return data_grid_widget(widget) if properties.empty? && widget["DataSource"]
422
+
234
423
  entity = properties.dig("datasource", "DataSource", "EntityRef", "Entity")
235
424
  column_type = custom_property_type(widget.dig("Type", "ObjectType"), "columns")
236
425
  &.dig("ValueType", "ObjectType")
@@ -244,12 +433,35 @@ module Mxrb
244
433
  end
245
434
  {
246
435
  type: :data_grid, name: widget["Name"],
247
- options: { entity: entity, columns: columns }.compact, events: []
436
+ options: { entity: entity, columns: columns }.compact, events: grid_events(widget)
248
437
  }
249
438
  end
250
439
 
440
+ def grid_events(widget)
441
+ {
442
+ "OnChangeAction" => :on_change,
443
+ "OnClickAction" => :on_click,
444
+ "Action" => :on_click
445
+ }.filter_map do |property, event|
446
+ action = parse_action(widget[property])
447
+ action&.merge(event: event)
448
+ end
449
+ end
450
+
251
451
  def combo_box_widget(widget)
252
452
  properties = custom_property_map(widget["Object"], widget.dig("Type", "ObjectType"))
453
+ if properties.empty? && widget["AttributePath"]
454
+ reference = widget["SelectorType"] == "Reference"
455
+ options = {
456
+ attribute: widget["AttributePath"], caption: extract_text(widget["LabelText"]),
457
+ display_attribute: widget["DisplayAttribute"]
458
+ }.compact
459
+ return {
460
+ type: reference ? :reference_selector : :drop_down,
461
+ name: widget["Name"], options: options, events: grid_events(widget)
462
+ }
463
+ end
464
+
253
465
  association = parse_array(properties.dig("attributeAssociation", "EntityRef", "Steps"))
254
466
  .first&.fetch("Association", nil)
255
467
  reference = properties.dig("optionsSourceType", "PrimitiveValue") == "association"
@@ -267,7 +479,7 @@ module Mxrb
267
479
  )
268
480
  end
269
481
  { type: reference ? :reference_selector : :drop_down,
270
- name: widget["Name"], options: options.compact, events: [] }
482
+ name: widget["Name"], options: options.compact, events: grid_events(widget) }
271
483
  end
272
484
 
273
485
  def custom_property_map(object, object_type)
@@ -376,21 +588,49 @@ module Mxrb
376
588
  def parse_source(source)
377
589
  return nil unless source.is_a?(Hash)
378
590
  result = if %w[Pages$NanoflowSource Forms$NanoflowSource].include?(source["$Type"])
379
- { kind: :nanoflow, name: local_name(source["Nanoflow"] || source.dig("NanoflowSettings", "Nanoflow")) }
591
+ { kind: :nanoflow, name: source_name(source["Nanoflow"] || source.dig("NanoflowSettings", "Nanoflow")) }
380
592
  else
381
- { kind: :microflow, name: local_name(source["Microflow"] || source.dig("MicroflowSettings", "Microflow")) }
593
+ { kind: :microflow, name: source_name(source["Microflow"] || source.dig("MicroflowSettings", "Microflow")) }
382
594
  end
383
595
  result[:name].to_s.empty? ? nil : result
384
596
  end
385
597
 
598
+ # Page data sources are architectural references. Unlike widget-local
599
+ # action handlers, their module qualification must survive export so a
600
+ # native Core.Flow reference cannot silently become Portal.Flow.
601
+ def source_name(name)
602
+ name.to_s
603
+ end
604
+
386
605
  def parse_action(action)
387
606
  return nil unless action.is_a?(Hash)
388
607
  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 }
608
+ settings = action["NanoflowSettings"] || action
609
+ handler = local_name(action["Nanoflow"] || settings["Nanoflow"])
610
+ return nil if handler.to_s.empty?
611
+
612
+ arguments = parse_array(settings["ParameterMappings"]).to_h do |mapping|
613
+ [local_name(mapping["Parameter"]), mapping["Expression"].to_s]
614
+ end
615
+ { kind: :nanoflow, handler: handler, arguments: arguments }
391
616
  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 }
617
+ settings = action["MicroflowSettings"] || action
618
+ handler = local_name(action["Microflow"] || settings["Microflow"])
619
+ return nil if handler.to_s.empty?
620
+
621
+ arguments = parse_array(settings["ParameterMappings"]).to_h do |mapping|
622
+ [local_name(mapping["Parameter"]), mapping["Expression"].to_s]
623
+ end
624
+ { kind: :microflow, handler: handler, arguments: arguments }
625
+ elsif %w[Pages$FormAction Forms$FormAction].include?(action["$Type"])
626
+ settings = action["FormSettings"] || action
627
+ handler = settings["Form"].to_s
628
+ return nil if handler.empty?
629
+
630
+ arguments = parse_array(settings["ParameterMappings"]).to_h do |mapping|
631
+ [local_name(mapping["Parameter"]), mapping["Expression"].to_s]
632
+ end
633
+ { kind: :page, handler: handler, arguments: arguments }
394
634
  elsif %w[Pages$SaveChangesClientAction Forms$SaveChangesClientAction].include?(action["$Type"])
395
635
  { kind: :action, handler: "save_changes" }
396
636
  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