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
@@ -5,18 +5,30 @@ require 'tmpdir'
5
5
 
6
6
  module Mxrb
7
7
  # Creates the minimum editable Ruby project accepted by `mxrb generate`.
8
- # rubocop:disable Metrics/ClassLength
8
+ # rubocop:disable Metrics
9
9
  class Initializer
10
10
  Result = Data.define(:root, :files)
11
11
  NAME = /\A[A-Za-z][A-Za-z0-9_-]*\z/
12
+ MODES = %i[mendix ruby].freeze
12
13
  COMPOUND_SUFFIXES = %w[service clinic portal demo api app kit].freeze
13
14
 
14
- def initialize(name, subject: :project, mxrb_path: nil)
15
+ attr_reader :mode
16
+
17
+ def initialize(name, subject: :project, mxrb_path: nil, mode: :mendix, stack: nil)
15
18
  @dir_name = name.to_s
16
19
  raise ArgumentError, "#{subject} name must be snake_case or PascalCase" unless NAME.match?(@dir_name)
17
20
 
18
21
  @module_name = to_pascal_case(@dir_name)
19
22
  @mpr_name = "#{@module_name}.mpr"
23
+ @mode = mode.to_sym
24
+ raise ArgumentError, 'project mode must be mendix or ruby' unless MODES.include?(@mode)
25
+
26
+ @stack = stack&.to_sym
27
+ raise ArgumentError, 'Ruby stack presets require --mode ruby' if @stack && @mode != :ruby
28
+ if @stack && !RubyApp::Preset::NAMES.include?(@stack)
29
+ raise ArgumentError, "Ruby stack must be #{RubyApp::Preset::NAMES.join(' or ')}"
30
+ end
31
+
20
32
  @mxrb_path = File.expand_path(mxrb_path) if mxrb_path
21
33
  raise ArgumentError, "mxrb path does not exist: #{@mxrb_path}" if @mxrb_path && !File.directory?(@mxrb_path)
22
34
  end
@@ -28,19 +40,61 @@ module Mxrb
28
40
 
29
41
  FileUtils.mkdir_p(parent)
30
42
  staging = Dir.mktmpdir(".#{@dir_name}.mxrb-init-", parent)
31
- write_scaffold(staging)
43
+ mode == :ruby ? write_ruby_scaffold(staging) : write_scaffold(staging)
44
+ files = mode == :ruby ? generated_files(staging) : relative_files
32
45
  FileUtils.mv(staging, root)
33
- Result.new(root, relative_files.map { File.join(root, _1) }.freeze)
46
+ Result.new(root, files.map { File.join(root, _1) }.freeze)
34
47
  ensure
35
48
  FileUtils.rm_rf(staging) if staging && File.exist?(staging)
36
49
  end
37
50
 
38
51
  private
39
52
 
53
+ def write_ruby_scaffold(root)
54
+ bootstrap = File.join(root, '.mxrb-bootstrap')
55
+ write_scaffold(bootstrap)
56
+ target = File.join(bootstrap, @mpr_name)
57
+ previous = ENV['MXRB_OUTPUT_PATH']
58
+ library = @mxrb_path ? File.join(@mxrb_path, 'lib') : File.expand_path('..', __dir__)
59
+ added_load_path = !$LOAD_PATH.include?(library)
60
+ $LOAD_PATH.unshift(library) if added_load_path
61
+ ENV['MXRB_OUTPUT_PATH'] = target
62
+ load File.join(bootstrap, 'project.rb')
63
+ Exporter.new(target, root, mode: :ruby).export!(parallel: false)
64
+ pin_exported_gemfile(root) if @mxrb_path
65
+ RubyApp::Preset.apply!(root, @stack) if @stack
66
+ ensure
67
+ if previous
68
+ ENV['MXRB_OUTPUT_PATH'] = previous
69
+ else
70
+ ENV.delete('MXRB_OUTPUT_PATH')
71
+ end
72
+ $LOAD_PATH.delete(library) if added_load_path
73
+ FileUtils.rm_rf(bootstrap)
74
+ end
75
+
76
+ def generated_files(root)
77
+ Dir.glob(File.join(root, '**', '*'), File::FNM_DOTMATCH)
78
+ .select { File.file?(_1) }
79
+ .map { Pathname.new(_1).relative_path_from(Pathname.new(root)).to_s }
80
+ .sort
81
+ end
82
+
83
+ def pin_exported_gemfile(root)
84
+ File.binwrite(
85
+ File.join(root, 'Gemfile'),
86
+ "# frozen_string_literal: true\n\nsource 'https://rubygems.org'\n" \
87
+ "gem 'mxrb', path: #{@mxrb_path.inspect}\n"
88
+ )
89
+ end
90
+
40
91
  def write_scaffold(root)
41
92
  write(root, 'Gemfile', gemfile)
42
93
  write(root, '.gitignore', gitignore)
43
94
  write(root, '.env.example', env_example)
95
+ environment_names.each do |name|
96
+ write(File.join(root, 'config', 'environments'), "#{name}.env.example", environment_example(name))
97
+ end
44
98
  write(root, 'project.rb', project_rb)
45
99
  write(File.join(root, 'app', 'navigation', 'responsive'), '.keep', '')
46
100
  write_design_scaffold(root)
@@ -79,7 +133,8 @@ module Mxrb
79
133
  ['Gemfile', '.gitignore', '.env.example', 'project.rb',
80
134
  *relative_module_files.map { File.join(module_prefix, _1) },
81
135
  File.join('app', 'navigation', 'responsive', '.keep'),
82
- *design_files.keys.map { File.join(*_1) }]
136
+ *design_files.keys.map { File.join(*_1) },
137
+ *environment_names.map { File.join('config', 'environments', "#{_1}.env.example") }]
83
138
  end
84
139
 
85
140
  def relative_module_files
@@ -121,7 +176,6 @@ module Mxrb
121
176
  source "https://rubygems.org"
122
177
 
123
178
  #{gem_declaration}
124
- gem "dotenv", "~> 3.0"
125
179
  RUBY
126
180
  end
127
181
 
@@ -131,6 +185,8 @@ module Mxrb
131
185
  .env
132
186
  .env.*
133
187
  !.env.example
188
+ config/environments/*.env
189
+ !config/environments/*.env.example
134
190
  TEXT
135
191
  end
136
192
 
@@ -141,6 +197,21 @@ module Mxrb
141
197
  TEXT
142
198
  end
143
199
 
200
+ def environment_names = %w[development qa staging production]
201
+
202
+ def environment_example(name)
203
+ <<~TEXT
204
+ # Copy to #{name}.env. Process ENV still has highest precedence.
205
+ MXRB_DATABASE_PATH=.mxrb/runtime/#{name}.sqlite3
206
+ MXRB_SHARED_STORE_PATH=.mxrb/runtime/#{name}-shared.sqlite3
207
+ MXRB_SESSION_TTL=3600
208
+ MXRB_SCHEDULER_LEASE_TTL=300
209
+ MXRB_ALLOW_DESTRUCTIVE_MIGRATIONS=false
210
+ MXRB_AUTH_TOKENS=
211
+ MXRB_USERS_JSON=
212
+ TEXT
213
+ end
214
+
144
215
  def gem_declaration
145
216
  return 'gem "mxrb"' unless @mxrb_path
146
217
 
@@ -151,10 +222,10 @@ module Mxrb
151
222
  <<~RUBY
152
223
  # frozen_string_literal: true
153
224
 
154
- require "dotenv/load"
155
- Dotenv.load(File.join(__dir__, ".env"))
156
225
  require "mxrb"
157
226
 
227
+ Mxrb::Environment.load(root: __dir__).apply
228
+
158
229
  output = ENV.fetch("MXRB_OUTPUT_PATH", File.join(__dir__, "#{@mpr_name}"))
159
230
 
160
231
  Mxrb.define(output) do
@@ -254,5 +325,5 @@ module Mxrb
254
325
  RUBY
255
326
  end
256
327
  end
257
- # rubocop:enable Metrics/ClassLength
328
+ # rubocop:enable Metrics
258
329
  end
@@ -11,19 +11,26 @@ module Mxrb
11
11
  class Validator
12
12
  def initialize(path)
13
13
  @path = path
14
+ @progress = Progress::NullTask.instance
14
15
  end
15
16
 
16
17
  def validate
17
- @errors = []
18
- @warnings = []
19
- @mpr = IO::MprFile.open(@path, readonly: true)
20
- validate_tables
21
- validate_units
22
- validate_v2_files
23
- Result.new(errors: @errors, warnings: @warnings)
18
+ Progress.with("Validating #{File.basename(@path)}") do |progress|
19
+ @progress = progress
20
+ @errors = []
21
+ @warnings = []
22
+ @mpr = IO::MprFile.open(@path, readonly: true)
23
+ validate_tables
24
+ progress.advance(detail: "MPR tables")
25
+ validate_units
26
+ validate_v2_files
27
+ progress.advance(detail: "v2 contents")
28
+ Result.new(errors: @errors, warnings: @warnings)
29
+ end
24
30
  rescue Error => e
25
31
  Result.new(errors: [e.message], warnings: @warnings || [])
26
32
  ensure
33
+ @progress = Progress::NullTask.instance
27
34
  @mpr&.close
28
35
  end
29
36
 
@@ -40,8 +47,13 @@ module Mxrb
40
47
 
41
48
  def validate_units
42
49
  @units = @mpr.all_units
50
+ @legacy_identity_mismatches = @mpr.legacy_unit_identity_mismatches.to_set do |entry|
51
+ [entry.fetch(:unit_id), entry.fetch(:content_id), entry.fetch(:type)]
52
+ end
53
+ @progress.update(total: @units.size + 3, detail: "#{@units.size} units")
43
54
  validate_root
44
55
  validate_unit_ids
56
+ @progress.advance(detail: "unit tree")
45
57
  validate_contents
46
58
  end
47
59
 
@@ -78,6 +90,8 @@ module Mxrb
78
90
  next unless doc
79
91
 
80
92
  validate_doc_identity(unit, doc)
93
+ ensure
94
+ @progress&.advance(detail: "unit #{unit['UnitID']}")
81
95
  end
82
96
  end
83
97
 
@@ -102,6 +116,17 @@ module Mxrb
102
116
  if doc_id.to_s.empty?
103
117
  add_error("unit #{unit['UnitID']} missing $ID")
104
118
  elsif doc_id != unit["UnitID"]
119
+ record_identity_mismatch(unit, doc_id, type)
120
+ end
121
+ end
122
+
123
+ def record_identity_mismatch(unit, doc_id, type)
124
+ mismatch = [unit["UnitID"], doc_id, type]
125
+ if @legacy_identity_mismatches&.include?(mismatch)
126
+ add_warning(
127
+ "unit #{unit['UnitID']} preserves legacy content $ID mismatch #{doc_id}"
128
+ )
129
+ else
105
130
  add_error("unit #{unit['UnitID']} content $ID mismatch #{doc_id}")
106
131
  end
107
132
  end
@@ -11,7 +11,7 @@ module Mxrb
11
11
  # - Arrays have a 4-byte int32 marker as first element (1=by-name, 2=part-secondary, 3=part-primary)
12
12
  # - $ID can appear as String, BSON Binary, or {Data:, Subtype:} map
13
13
  # - $Type uses storage names ("DomainModels$Entity") not SDK qualified names
14
- module BsonCodec
14
+ module BsonCodec # rubocop:disable Metrics/ModuleLength
15
15
  # ── UUID / BLOB conversion ─────────────────────────────────────────────
16
16
 
17
17
  # Convert a 16-byte MS-GUID blob to UUID string.
@@ -49,12 +49,53 @@ module Mxrb
49
49
  when BSON::Binary
50
50
  blob_to_uuid(value.data)
51
51
  when Hash
52
+ binary = extended_binary(value)
53
+ return blob_to_uuid(binary.data) if binary
54
+
52
55
  data = value["Data"] || value[:Data]
53
56
  return nil unless data
54
57
  blob_to_uuid(Base64.strict_decode64(data.to_s))
55
58
  end
56
59
  end
57
60
 
61
+ BINARY_SUBTYPES = {
62
+ "00" => :generic, "01" => :function, "02" => :old_binary,
63
+ "03" => :uuid_old, "04" => :uuid, "05" => :md5,
64
+ "80" => :user
65
+ }.freeze
66
+
67
+ # Restores BSON values serialized through JSON's canonical extended form.
68
+ # Architecture metadata uses JSON, so this conversion is required before
69
+ # a stored native document is written again during a version transition.
70
+ def self.restore_extended_json(value)
71
+ case value
72
+ when Hash
73
+ binary = extended_binary(value)
74
+ return binary if binary
75
+
76
+ value.to_h { |key, child| [key, restore_extended_json(child)] }
77
+ when Array
78
+ value.map { restore_extended_json(_1) }
79
+ else
80
+ value
81
+ end
82
+ end
83
+
84
+ def self.extended_binary(value)
85
+ payload = value["$binary"] || value[:$binary]
86
+ return unless payload.is_a?(Hash)
87
+
88
+ base64 = payload["base64"] || payload[:base64]
89
+ return unless base64
90
+
91
+ subtype = payload["subType"] || payload[:subType] || "00"
92
+ BSON::Binary.new(
93
+ Base64.strict_decode64(base64.to_s.delete(" \t\r\n")),
94
+ BINARY_SUBTYPES.fetch(subtype.to_s.downcase, :generic)
95
+ )
96
+ end
97
+ private_class_method :extended_binary
98
+
58
99
  # ── Array parsing ───────────────────────────────────────────────────────
59
100
 
60
101
  # Mendix arrays have an int32 marker as first element.
@@ -162,10 +162,10 @@ module Mxrb
162
162
  # ── Writes ────────────────────────────────────────────────────────────
163
163
 
164
164
  # Insert a new unit. Returns the assigned UUID.
165
- def insert_unit(container_uuid:, containment_name:, contents_doc:)
165
+ def insert_unit(container_uuid:, containment_name:, contents_doc:, unit_uuid: nil)
166
166
  raise ReadOnlyError, "Opened in read-only mode" if @readonly
167
167
 
168
- uuid = BsonCodec.extract_id(contents_doc["$ID"] || contents_doc["\$ID"]) || SecureRandom.uuid
168
+ uuid = unit_uuid || BsonCodec.extract_id(contents_doc["$ID"] || contents_doc["\$ID"]) || SecureRandom.uuid
169
169
  unless contents_doc.key?("$ID") || contents_doc.key?("\$ID")
170
170
  contents_doc = { "$ID" => uuid }.merge(contents_doc)
171
171
  end
@@ -223,6 +223,31 @@ module Mxrb
223
223
  true
224
224
  end
225
225
 
226
+ # Repairs stale Unit.ContentsHash metadata without reserializing or
227
+ # otherwise changing the BSON/mxunit payload. Returns an audit trail.
228
+ def repair_content_hashes!
229
+ raise ReadOnlyError, "Opened in read-only mode" if @readonly
230
+
231
+ repairs = []
232
+ transaction do
233
+ all_units.each do |unit|
234
+ bytes = content_bytes(unit)
235
+ next if bytes.to_s.empty?
236
+
237
+ previous = unit['ContentsHash'].to_s
238
+ current = BsonCodec.contents_hash(bytes)
239
+ next if previous == current
240
+
241
+ @db.execute(
242
+ 'UPDATE Unit SET ContentsHash = ? WHERE UnitID = ?',
243
+ [current, BsonCodec.uuid_to_blob(unit.fetch('UnitID'))]
244
+ )
245
+ repairs << { unit_id: unit.fetch('UnitID'), previous:, current: }
246
+ end
247
+ end
248
+ repairs
249
+ end
250
+
226
251
  def delete_unit(uuid)
227
252
  raise ReadOnlyError, "Opened in read-only mode" if @readonly
228
253
  @db.execute("DELETE FROM Unit WHERE UnitID = ?", [BsonCodec.uuid_to_blob(uuid)])
@@ -463,7 +488,7 @@ module Mxrb
463
488
  def architecture_definition
464
489
  return nil unless tables.include?("_MxrbArchitecture")
465
490
  json = @db.get_first_value("SELECT Definition FROM _MxrbArchitecture WHERE ID = 1")
466
- json && JSON.parse(json, symbolize_names: true)
491
+ json && BsonCodec.restore_extended_json(JSON.parse(json, symbolize_names: true))
467
492
  end
468
493
 
469
494
  def write_architecture_definition(definition)
@@ -481,6 +506,124 @@ module Mxrb
481
506
  )
482
507
  end
483
508
 
509
+ # Ruby/React sources are stored outside the Mendix Unit tree. Mendix can
510
+ # keep editing the native model while MXRB can later restore the exact
511
+ # conventional application sources on a Ruby-mode export.
512
+ def ruby_app_sources
513
+ return [] unless tables.include?("_MxrbRubySource")
514
+
515
+ has_mode = table_info("_MxrbRubySource").any? { (_1["name"] || _1[:name]) == "Mode" }
516
+ columns = has_mode ? "Path, Contents, Sha256, Mode" : "Path, Contents, Sha256"
517
+ @db.execute("SELECT #{columns} FROM _MxrbRubySource ORDER BY Path").map do |row|
518
+ fallback = row[0].to_s.start_with?('bin/') ? 0o755 : 0o644
519
+ { path: row[0], contents: row[1], sha256: row[2], mode: has_mode ? row[3] : fallback }
520
+ end
521
+ end
522
+
523
+ def write_ruby_app_sources(files)
524
+ raise ReadOnlyError, "Opened in read-only mode" if @readonly
525
+
526
+ @db.execute(<<~SQL)
527
+ CREATE TABLE IF NOT EXISTS _MxrbRubySource (
528
+ Path TEXT PRIMARY KEY NOT NULL,
529
+ Contents BLOB NOT NULL,
530
+ Sha256 TEXT NOT NULL,
531
+ Mode INTEGER NOT NULL DEFAULT 420
532
+ )
533
+ SQL
534
+ unless table_info("_MxrbRubySource").any? { (_1["name"] || _1[:name]) == "Mode" }
535
+ @db.execute("ALTER TABLE _MxrbRubySource ADD COLUMN Mode INTEGER NOT NULL DEFAULT 420")
536
+ end
537
+ @db.execute("DELETE FROM _MxrbRubySource")
538
+ files.each do |file|
539
+ @db.execute(
540
+ "INSERT INTO _MxrbRubySource (Path, Contents, Sha256, Mode) VALUES (?, ?, ?, ?)",
541
+ [
542
+ file.fetch(:path), SQLite3::Blob.new(file.fetch(:contents)),
543
+ file.fetch(:sha256), file.fetch(:mode, 0o644)
544
+ ]
545
+ )
546
+ end
547
+ end
548
+
549
+ # Cross-module Mendix associations do not expose native visual
550
+ # connection fields. Keep their ER-editor anchors in an MXRB-only table
551
+ # so the diagram remains editable without inventing unsupported BSON.
552
+ def domain_diagram_anchors
553
+ return {} unless tables.include?("_MxrbDomainDiagramAssociation")
554
+
555
+ @db.execute(<<~SQL).to_h do |row|
556
+ SELECT AssociationID, SourceAnchor, TargetAnchor
557
+ FROM _MxrbDomainDiagramAssociation
558
+ SQL
559
+ [row[0], { source_anchor: row[1], target_anchor: row[2] }]
560
+ end
561
+ end
562
+
563
+ def write_domain_diagram_anchors(layouts)
564
+ raise ReadOnlyError, "Opened in read-only mode" if @readonly
565
+ items = Array(layouts)
566
+ return 0 if items.empty?
567
+
568
+ @db.execute(<<~SQL)
569
+ CREATE TABLE IF NOT EXISTS _MxrbDomainDiagramAssociation (
570
+ AssociationID TEXT PRIMARY KEY NOT NULL,
571
+ SourceAnchor TEXT NOT NULL,
572
+ TargetAnchor TEXT NOT NULL
573
+ )
574
+ SQL
575
+ current = domain_diagram_anchors
576
+ items.count do |layout|
577
+ id = layout.fetch(:id).to_s
578
+ anchors = {
579
+ source_anchor: layout.fetch(:source_anchor).to_s,
580
+ target_anchor: layout.fetch(:target_anchor).to_s
581
+ }
582
+ next false if current[id] == anchors
583
+
584
+ @db.execute(
585
+ "INSERT OR REPLACE INTO _MxrbDomainDiagramAssociation " \
586
+ "(AssociationID, SourceAnchor, TargetAnchor) VALUES (?, ?, ?)",
587
+ [id, anchors.fetch(:source_anchor), anchors.fetch(:target_anchor)]
588
+ )
589
+ true
590
+ end
591
+ end
592
+
593
+ def legacy_unit_identity_mismatches
594
+ return [] unless tables.include?("_MxrbCompatibility")
595
+
596
+ @db.execute(<<~SQL).map do |row|
597
+ SELECT UnitID, ContentID, UnitType
598
+ FROM _MxrbCompatibility
599
+ WHERE Kind = 'legacy-unit-identity'
600
+ SQL
601
+ { unit_id: row[0], content_id: row[1], type: row[2] }
602
+ end
603
+ end
604
+
605
+ def write_legacy_unit_identity_mismatches(mismatches)
606
+ raise ReadOnlyError, "Opened in read-only mode" if @readonly
607
+
608
+ @db.execute(<<~SQL)
609
+ CREATE TABLE IF NOT EXISTS _MxrbCompatibility (
610
+ Kind TEXT NOT NULL,
611
+ UnitID TEXT NOT NULL,
612
+ ContentID TEXT NOT NULL,
613
+ UnitType TEXT NOT NULL,
614
+ PRIMARY KEY (Kind, UnitID, ContentID)
615
+ )
616
+ SQL
617
+ @db.execute("DELETE FROM _MxrbCompatibility WHERE Kind = 'legacy-unit-identity'")
618
+ mismatches.each do |mismatch|
619
+ @db.execute(
620
+ "INSERT INTO _MxrbCompatibility (Kind, UnitID, ContentID, UnitType) VALUES (?, ?, ?, ?)",
621
+ ["legacy-unit-identity", mismatch.fetch(:unit_id),
622
+ mismatch.fetch(:content_id), mismatch.fetch(:type)]
623
+ )
624
+ end
625
+ end
626
+
484
627
  def close
485
628
  @db.close
486
629
  end
@@ -7,6 +7,8 @@ module Mxrb
7
7
  # Attributes are embedded in Entity BSON — NOT separate Unit rows.
8
8
  # $Type: DomainModels$Attribute
9
9
  class Attribute
10
+ DEFAULT_STRING_LENGTH = 200
11
+
10
12
  # Maps DSL symbol → Mendix storage type name
11
13
  TYPE_MAP = {
12
14
  string: "DomainModels$StringAttributeType",
@@ -84,6 +86,7 @@ module Mxrb
84
86
  def serialize_type
85
87
  storage_type = TYPE_MAP[@type] || TYPE_MAP[:string]
86
88
  base = { "$ID" => SecureRandom.uuid, "$Type" => storage_type }
89
+ base["length"] = @length || DEFAULT_STRING_LENGTH if storage_type == TYPE_MAP[:string]
87
90
  base["localizeDate"] = true if @type == :datetime
88
91
  base
89
92
  end
@@ -10,7 +10,7 @@ module Mxrb
10
10
  attr_accessor :id, :name, :qualified_name, :documentation,
11
11
  :persistable, :location, :data_storage_guid,
12
12
  :export_level, :generalization, :access_rules, :indexes,
13
- :system_members
13
+ :system_members, :lifecycle, :source, :oql_query, :native_type
14
14
 
15
15
  # Build from a BSON hash (embedded in DomainModel's "entities" array).
16
16
  def self.from_bson(doc, _domain_model_id, mpr)
@@ -19,6 +19,9 @@ module Mxrb
19
19
  e.name = doc["name"] || doc["Name"]
20
20
  e.qualified_name = doc["$QualifiedName"] || doc["\$QualifiedName"]
21
21
  e.documentation = doc["documentation"] || doc["Documentation"] || ""
22
+ e.native_type = doc["$Type"]
23
+ e.source = doc["source"] || doc["Source"]
24
+ e.oql_query = doc["oqlQuery"] || doc["OqlQuery"] || doc["OQLQuery"]
22
25
  e.data_storage_guid = doc["dataStorageGuid"] || doc["DataStorageGuid"]
23
26
  e.export_level = doc["exportLevel"] || doc["ExportLevel"] || "Hidden"
24
27
  e.location = parse_location(doc["location"] || doc["Location"])
@@ -46,6 +49,8 @@ module Mxrb
46
49
  # Access rules (embedded array with marker 3)
47
50
  rule_arr = IO::BsonCodec.parse_array(doc["accessRules"] || doc["AccessRules"])[:items]
48
51
  e.access_rules = rule_arr.map { parse_access_rule(_1) }
52
+ event_arr = IO::BsonCodec.parse_array(doc["eventHandlers"] || doc["EventHandlers"])[:items]
53
+ e.lifecycle = event_arr.map { parse_lifecycle(_1) }
49
54
 
50
55
  e
51
56
  end
@@ -58,6 +63,19 @@ module Mxrb
58
63
  @generalization['generalization'] || @generalization['Generalization']
59
64
  end
60
65
 
66
+ def oql_view?
67
+ source_type = @source.is_a?(Hash) ? @source.fetch('$Type', '') : ''
68
+ @native_type.to_s.match?(/ViewEntity/i) ||
69
+ source_type.match?(/OqlViewEntitySource/i) ||
70
+ !@oql_query.to_s.empty?
71
+ end
72
+
73
+ def oql_source_document
74
+ return unless @source.is_a?(Hash)
75
+
76
+ @source['SourceDocument'] || @source['sourceDocument']
77
+ end
78
+
61
79
  def to_bson
62
80
  {
63
81
  "$ID" => @id,
@@ -70,7 +88,7 @@ module Mxrb
70
88
  "generalization" => serialize_generalization,
71
89
  "attributes" => IO::BsonCodec.build_array(@attributes.map(&:to_bson)),
72
90
  "validationRules" => IO::BsonCodec.build_array([]), # must come after attributes
73
- "eventHandlers" => IO::BsonCodec.build_array([]),
91
+ "eventHandlers" => IO::BsonCodec.build_array(@lifecycle.to_a.map { lifecycle_bson(_1) }),
74
92
  "indexes" => IO::BsonCodec.build_array([]),
75
93
  "accessRules" => IO::BsonCodec.build_array(@access_rules.to_a),
76
94
  "source" => nil,
@@ -150,6 +168,17 @@ module Mxrb
150
168
  }
151
169
  end
152
170
 
171
+ def self.parse_lifecycle(doc)
172
+ moment = doc["Moment"].to_s.downcase
173
+ event = doc["Event"].to_s.downcase
174
+ {
175
+ event: [moment, event].reject(&:empty?).join("_").to_sym,
176
+ handler: doc["Microflow"].to_s,
177
+ pass_event_object: doc.fetch("PassEventObject", true) == true,
178
+ raise_error_on_false: doc["RaiseErrorOnFalse"] == true
179
+ }
180
+ end
181
+
153
182
  def self.parse_location(loc)
154
183
  if loc.is_a?(String)
155
184
  x, y = loc.split(';', 2).map(&:to_i)
@@ -175,6 +204,19 @@ module Mxrb
175
204
  "hasChangedBy" => false,
176
205
  }
177
206
  end
207
+
208
+ def lifecycle_bson(callback)
209
+ moment, event = callback.fetch(:event).to_s.split("_", 2)
210
+ {
211
+ "$ID" => SecureRandom.uuid,
212
+ "$Type" => "DomainModels$EventHandler",
213
+ "Event" => event.to_s.capitalize,
214
+ "Moment" => moment.to_s.capitalize,
215
+ "Microflow" => callback.fetch(:handler).to_s,
216
+ "PassEventObject" => callback.fetch(:pass_event_object, true) == true,
217
+ "RaiseErrorOnFalse" => callback.fetch(:raise_error_on_false, false) == true
218
+ }
219
+ end
178
220
  end
179
221
  end
180
222
  end
@@ -8,7 +8,7 @@ module Mxrb
8
8
  # ContainmentName: "Documents"
9
9
  class Microflow < Unit
10
10
  attr_reader :name, :documentation, :return_variable_name,
11
- :allow_concurrent_execution, :mark_as_used, :excluded,
11
+ :allow_concurrent_execution, :apply_entity_access, :mark_as_used, :excluded,
12
12
  :allowed_module_roles, :parameters, :return_type,
13
13
  :objects, :flows
14
14
 
@@ -17,6 +17,7 @@ module Mxrb
17
17
  @documentation = doc["Documentation"] || doc["documentation"] || ""
18
18
  @return_variable_name = doc["ReturnVariableName"] || "ReturnValue"
19
19
  @allow_concurrent_execution = doc["AllowConcurrentExecution"] != false
20
+ @apply_entity_access = doc["ApplyEntityAccess"] == true
20
21
  @mark_as_used = doc["MarkAsUsed"] == true
21
22
  @excluded = doc["Excluded"] == true
22
23
  @allowed_module_roles = parse_array(doc["AllowedModuleRoles"])
@@ -43,6 +44,7 @@ module Mxrb
43
44
  "Documentation" => @documentation || "",
44
45
  "ReturnVariableName" => @return_variable_name || "ReturnValue",
45
46
  "AllowConcurrentExecution" => @allow_concurrent_execution != false,
47
+ "ApplyEntityAccess" => @apply_entity_access == true,
46
48
  "MarkAsUsed" => @mark_as_used || false,
47
49
  "Excluded" => @excluded || false,
48
50
  "AllowedModuleRoles" => IO::BsonCodec.build_array(@allowed_module_roles || [], marker: 1),