mxrb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.de-DE.md +108 -0
  4. data/README.md +172 -0
  5. data/README.pt-BR.md +109 -0
  6. data/bin/mxrb +1613 -0
  7. data/docs/README.md +10 -0
  8. data/docs/de-DE/README.md +24 -0
  9. data/docs/de-DE/architectural-patterns.md +26 -0
  10. data/docs/de-DE/architectural-standard.md +48 -0
  11. data/docs/de-DE/architecture.md +76 -0
  12. data/docs/de-DE/compiler.md +162 -0
  13. data/docs/de-DE/conventions.md +19 -0
  14. data/docs/de-DE/design-system.md +52 -0
  15. data/docs/de-DE/entity-dsl.md +53 -0
  16. data/docs/de-DE/native-runtime-quality-report.md +74 -0
  17. data/docs/de-DE/oql-sql.md +211 -0
  18. data/docs/de-DE/platform-api-integration.md +36 -0
  19. data/docs/de-DE/platform-operations.md +243 -0
  20. data/docs/de-DE/project-structure.md +32 -0
  21. data/docs/de-DE/ruby-first-roadmap.md +116 -0
  22. data/docs/de-DE/scaffolds.md +59 -0
  23. data/docs/de-DE/semantic-refactoring.md +56 -0
  24. data/docs/de-DE/team-server.md +33 -0
  25. data/docs/de-DE/validation-matrix.md +64 -0
  26. data/docs/de-DE/vetclinic-acceptance.md +24 -0
  27. data/docs/de-DE/writing.md +138 -0
  28. data/docs/en-US/README.md +24 -0
  29. data/docs/en-US/architectural-patterns.md +151 -0
  30. data/docs/en-US/architectural-standard.md +89 -0
  31. data/docs/en-US/architecture.md +170 -0
  32. data/docs/en-US/compiler.md +157 -0
  33. data/docs/en-US/conventions.md +82 -0
  34. data/docs/en-US/design-system.md +163 -0
  35. data/docs/en-US/entity-dsl.md +53 -0
  36. data/docs/en-US/native-runtime-quality-report.md +73 -0
  37. data/docs/en-US/oql-sql.md +201 -0
  38. data/docs/en-US/platform-api-integration.md +36 -0
  39. data/docs/en-US/platform-operations.md +244 -0
  40. data/docs/en-US/project-structure.md +49 -0
  41. data/docs/en-US/ruby-first-roadmap.md +186 -0
  42. data/docs/en-US/scaffolds.md +57 -0
  43. data/docs/en-US/semantic-refactoring.md +198 -0
  44. data/docs/en-US/team-server.md +32 -0
  45. data/docs/en-US/validation-matrix.md +257 -0
  46. data/docs/en-US/vetclinic-acceptance.md +23 -0
  47. data/docs/en-US/writing.md +655 -0
  48. data/docs/pt-BR/README.md +24 -0
  49. data/docs/pt-BR/architectural-patterns.md +25 -0
  50. data/docs/pt-BR/architectural-standard.md +533 -0
  51. data/docs/pt-BR/architecture.md +300 -0
  52. data/docs/pt-BR/compiler.md +162 -0
  53. data/docs/pt-BR/conventions.md +19 -0
  54. data/docs/pt-BR/design-system.md +65 -0
  55. data/docs/pt-BR/entity-dsl.md +116 -0
  56. data/docs/pt-BR/native-runtime-quality-report.md +73 -0
  57. data/docs/pt-BR/oql-sql.md +208 -0
  58. data/docs/pt-BR/platform-api-integration.md +36 -0
  59. data/docs/pt-BR/platform-operations.md +260 -0
  60. data/docs/pt-BR/project-structure.md +32 -0
  61. data/docs/pt-BR/ruby-first-roadmap.md +239 -0
  62. data/docs/pt-BR/scaffolds.md +104 -0
  63. data/docs/pt-BR/semantic-refactoring.md +55 -0
  64. data/docs/pt-BR/team-server.md +59 -0
  65. data/docs/pt-BR/validation-matrix.md +86 -0
  66. data/docs/pt-BR/vetclinic-acceptance.md +50 -0
  67. data/docs/pt-BR/writing.md +177 -0
  68. data/examples/sudoku_evaluation.rb +14 -0
  69. data/examples/sudoku_functional_test.rb +28 -0
  70. data/lib/mxrb/architecture/graph.rb +124 -0
  71. data/lib/mxrb/architecture/validator.rb +98 -0
  72. data/lib/mxrb/benchmark.rb +33 -0
  73. data/lib/mxrb/compare.rb +390 -0
  74. data/lib/mxrb/compiler/adapter.rb +96 -0
  75. data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
  76. data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
  77. data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
  78. data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
  79. data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
  80. data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
  81. data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
  82. data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
  83. data/lib/mxrb/compiler/constants_materializer.rb +111 -0
  84. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
  85. data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
  86. data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
  87. data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
  88. data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
  89. data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
  90. data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
  91. data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
  92. data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
  93. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
  94. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
  95. data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
  96. data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
  97. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
  98. data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
  99. data/lib/mxrb/compiler/mda.rb +99 -0
  100. data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
  101. data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
  102. data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
  103. data/lib/mxrb/compiler/model_package.rb +116 -0
  104. data/lib/mxrb/compiler/model_values.rb +67 -0
  105. data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
  106. data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
  107. data/lib/mxrb/compiler/packager.rb +100 -0
  108. data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
  109. data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
  110. data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
  111. data/lib/mxrb/compiler/portable_packager.rb +356 -0
  112. data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
  113. data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
  114. data/lib/mxrb/compiler/project_materializer.rb +139 -0
  115. data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
  116. data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
  117. data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
  118. data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
  119. data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
  120. data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
  121. data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
  122. data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
  123. data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
  124. data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
  125. data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
  126. data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
  127. data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
  128. data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
  129. data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
  130. data/lib/mxrb/compiler/security_materializer.rb +104 -0
  131. data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
  132. data/lib/mxrb/compiler/settings_materializer.rb +25 -0
  133. data/lib/mxrb/compiler/source_model.rb +128 -0
  134. data/lib/mxrb/compiler/system_model_seed.rb +88 -0
  135. data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
  136. data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
  137. data/lib/mxrb/compiler/translation_materializer.rb +95 -0
  138. data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
  139. data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
  140. data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
  141. data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
  142. data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
  143. data/lib/mxrb/doctor.rb +116 -0
  144. data/lib/mxrb/dsl/builder.rb +1636 -0
  145. data/lib/mxrb/errors.rb +18 -0
  146. data/lib/mxrb/evaluation.rb +131 -0
  147. data/lib/mxrb/exporter.rb +1716 -0
  148. data/lib/mxrb/frontend/migrator.rb +775 -0
  149. data/lib/mxrb/functional.rb +307 -0
  150. data/lib/mxrb/github/annotator.rb +161 -0
  151. data/lib/mxrb/initializer.rb +257 -0
  152. data/lib/mxrb/integrity/validator.rb +136 -0
  153. data/lib/mxrb/io/bson_codec.rb +145 -0
  154. data/lib/mxrb/io/mpr_file.rb +558 -0
  155. data/lib/mxrb/io/mxunit_codec.rb +42 -0
  156. data/lib/mxrb/marketplace.rb +380 -0
  157. data/lib/mxrb/model/association.rb +98 -0
  158. data/lib/mxrb/model/attribute.rb +100 -0
  159. data/lib/mxrb/model/connector.rb +17 -0
  160. data/lib/mxrb/model/design_materializer.rb +133 -0
  161. data/lib/mxrb/model/design_migration.rb +126 -0
  162. data/lib/mxrb/model/design_system.rb +111 -0
  163. data/lib/mxrb/model/domain_model.rb +51 -0
  164. data/lib/mxrb/model/entity.rb +180 -0
  165. data/lib/mxrb/model/menu.rb +38 -0
  166. data/lib/mxrb/model/microflow.rb +100 -0
  167. data/lib/mxrb/model/module.rb +142 -0
  168. data/lib/mxrb/model/navigation.rb +127 -0
  169. data/lib/mxrb/model/page.rb +410 -0
  170. data/lib/mxrb/model/project.rb +187 -0
  171. data/lib/mxrb/model/unit.rb +48 -0
  172. data/lib/mxrb/module_initializer.rb +75 -0
  173. data/lib/mxrb/official_marketplace/content_api.rb +292 -0
  174. data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
  175. data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
  176. data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
  177. data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
  178. data/lib/mxrb/official_marketplace.rb +720 -0
  179. data/lib/mxrb/oql/analyzer.rb +155 -0
  180. data/lib/mxrb/oql/index_advisor.rb +85 -0
  181. data/lib/mxrb/oql/plan_analyzer.rb +185 -0
  182. data/lib/mxrb/oql/server.rb +132 -0
  183. data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
  184. data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
  185. data/lib/mxrb/oql/workload_analyzer.rb +175 -0
  186. data/lib/mxrb/oql/workload_baseline.rb +60 -0
  187. data/lib/mxrb/oql.rb +424 -0
  188. data/lib/mxrb/project_lifecycle.rb +76 -0
  189. data/lib/mxrb/protocols/plan.rb +86 -0
  190. data/lib/mxrb/protocols.rb +123 -0
  191. data/lib/mxrb/runtime/database_workspace.rb +677 -0
  192. data/lib/mxrb/runtime/docker_executor.rb +41 -0
  193. data/lib/mxrb/runtime/docker_workspace.rb +29 -0
  194. data/lib/mxrb/runtime/executor.rb +169 -0
  195. data/lib/mxrb/runtime/java_locator.rb +26 -0
  196. data/lib/mxrb/runtime/native.rb +474 -0
  197. data/lib/mxrb/runtime/sql_server_database.rb +146 -0
  198. data/lib/mxrb/runtime/toolchain.rb +82 -0
  199. data/lib/mxrb/scaffold/cli.rb +113 -0
  200. data/lib/mxrb/scaffold/generator.rb +246 -0
  201. data/lib/mxrb/scaffold/help.rb +80 -0
  202. data/lib/mxrb/scaffold/page_templates.rb +54 -0
  203. data/lib/mxrb/scaffold/recipes.rb +198 -0
  204. data/lib/mxrb/scaffold/registry.rb +82 -0
  205. data/lib/mxrb/scaffold/templates.rb +723 -0
  206. data/lib/mxrb/scaffold/transaction.rb +84 -0
  207. data/lib/mxrb/schema/tables.rb +55 -0
  208. data/lib/mxrb/semantic/analyzer.rb +473 -0
  209. data/lib/mxrb/semantic/batch_plan.rb +68 -0
  210. data/lib/mxrb/semantic/domain_mutator.rb +419 -0
  211. data/lib/mxrb/semantic/embedder.rb +51 -0
  212. data/lib/mxrb/semantic/extractor.rb +290 -0
  213. data/lib/mxrb/semantic/index.rb +623 -0
  214. data/lib/mxrb/semantic/inliner.rb +216 -0
  215. data/lib/mxrb/semantic/mover.rb +153 -0
  216. data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
  217. data/lib/mxrb/semantic/remover.rb +70 -0
  218. data/lib/mxrb/semantic/renamer.rb +211 -0
  219. data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
  220. data/lib/mxrb/semantic/vec_store.rb +75 -0
  221. data/lib/mxrb/team_server.rb +390 -0
  222. data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
  223. data/lib/mxrb/templates/project/11.12.1.json +32 -0
  224. data/lib/mxrb/templates/project/6.10.8.json +50 -0
  225. data/lib/mxrb/templates/project/7.17.0.json +50 -0
  226. data/lib/mxrb/templates/project/7.5.0.json +50 -0
  227. data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
  228. data/lib/mxrb/version.rb +5 -0
  229. data/lib/mxrb/widget_package.rb +338 -0
  230. data/lib/mxrb/widget_synchronizer.rb +31 -0
  231. data/lib/mxrb/writer.rb +3775 -0
  232. data/lib/mxrb.rb +188 -0
  233. data/marketplace/catalog.json +10 -0
  234. data/marketplace/modules/shared-kernel/domain/README.md +4 -0
  235. data/marketplace/modules/shared-kernel/module.rb +5 -0
  236. data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
  237. metadata +447 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+ require 'tmpdir'
6
+
7
+ module Mxrb
8
+ module Compiler
9
+ ConstantsMaterialization = Data.define(:model_path, :metadata_path, :constants)
10
+
11
+ # Compiles MPR constants into Runtime model and metadata representations.
12
+ class ConstantsMaterializer
13
+ DATA_TYPES = {
14
+ 'DataTypes$StringType' => 'String', 'DataTypes$IntegerType' => 'Integer',
15
+ 'DataTypes$BooleanType' => 'Boolean', 'DataTypes$DecimalType' => 'Decimal',
16
+ 'DataTypes$DateTimeType' => 'DateTime'
17
+ }.freeze
18
+
19
+ def initialize(mpr_path, deployment:)
20
+ @mpr_path = File.expand_path(mpr_path)
21
+ @deployment = File.expand_path(deployment)
22
+ end
23
+
24
+ def materialize
25
+ source = SourceModel.read(@mpr_path)
26
+ constants = compile(source)
27
+ model_path = File.join(@deployment, 'model', 'model.mdp')
28
+ metadata_path = File.join(@deployment, 'model', 'metadata.json')
29
+ write_model(model_path, constants)
30
+ metadata_constants = write_metadata(metadata_path, constants)
31
+ ConstantsMaterialization.new(
32
+ model_path:, metadata_path:, constants: metadata_constants.freeze
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def compile(source)
39
+ source.units_of('Constants$Constant').map do |unit|
40
+ raise CompilationError, "constant #{unit.document['Name']} is outside a module" unless unit.module_name
41
+
42
+ compile_constant(source, unit)
43
+ end
44
+ end
45
+
46
+ def compile_constant(source, unit)
47
+ document = unit.document
48
+ qualified_name = "#{unit.module_name}.#{document['Name']}"
49
+ data_type = constant_type(document, qualified_name)
50
+ {
51
+ module_name: unit.module_name, runtime: runtime_document(document, qualified_name, data_type, source),
52
+ metadata: metadata_document(document, qualified_name, data_type)
53
+ }
54
+ end
55
+
56
+ def constant_type(document, qualified_name)
57
+ native = document.dig('Type', '$Type')
58
+ legacy = document['DataType'].to_s
59
+ return legacy if DATA_TYPES.value?(legacy)
60
+
61
+ DATA_TYPES.fetch(native) do
62
+ raise CompilationError, "unsupported constant type #{native.inspect} for #{qualified_name}"
63
+ end
64
+ end
65
+
66
+ def runtime_document(source, qualified_name, data_type, model)
67
+ {
68
+ '$ID' => source['$ID'], '$Type' => source['$Type'], 'Name' => source['Name'],
69
+ 'QualifiedName' => qualified_name, 'DataType' => data_type,
70
+ 'ExposedToClient' => model.client_reference?(qualified_name)
71
+ }
72
+ end
73
+
74
+ def metadata_document(source, qualified_name, data_type)
75
+ {
76
+ 'Name' => qualified_name, 'Type' => data_type,
77
+ 'Description' => source['Documentation'].to_s,
78
+ 'DefaultValue' => source['DefaultValue'].to_s
79
+ }
80
+ end
81
+
82
+ def write_metadata(path, constants)
83
+ metadata = JSON.parse(File.read(path))
84
+ metadata['Constants'] = ordered_metadata(constants)
85
+ atomic_json_write(path, metadata)
86
+ metadata.fetch('Constants')
87
+ end
88
+
89
+ def ordered_metadata(constants)
90
+ constants.group_by { _1.fetch(:module_name) }
91
+ .flat_map { |_module, entries| entries.sort_by { _1.dig(:metadata, 'Name') } }
92
+ .map { _1.fetch(:metadata) }
93
+ end
94
+
95
+ def write_model(path, constants)
96
+ package = constants.reduce(ModelPackage.read(path)) do |result, constant|
97
+ result.upsert(constant.fetch(:runtime))
98
+ end
99
+ package.write(path)
100
+ end
101
+
102
+ def atomic_json_write(path, document)
103
+ Dir.mktmpdir('mxrb-metadata-', File.dirname(path)) do |directory|
104
+ temporary = File.join(directory, 'metadata.json')
105
+ File.write(temporary, JSON.pretty_generate(document))
106
+ FileUtils.mv(temporary, path, force: true)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Mxrb
6
+ module Compiler
7
+ # Compiles the Data Grid 2 XPath/attribute-column subset into pluggable-widget properties.
8
+ # rubocop:disable Metrics
9
+ class DataGridBundleCompiler
10
+ include ModelValues
11
+
12
+ WIDGET_ID = 'com.mendix.widget.web.datagrid.Datagrid'
13
+
14
+ def initialize(source, page_name, widget = nil, render_widgets: nil, **widget_keywords)
15
+ @source = source
16
+ @page_name = page_name
17
+ @widget = widget || widget_keywords
18
+ @render_widgets = render_widgets
19
+ @index = document_index
20
+ end
21
+
22
+ def supported?
23
+ widget_type&.fetch('WidgetId', nil) == WIDGET_ID && xpath_source &&
24
+ columns.all? { supported_column?(_1) }
25
+ end
26
+
27
+ def render
28
+ "React.createElement($Datagrid, #{js_object(properties)})"
29
+ end
30
+
31
+ private
32
+
33
+ def properties
34
+ values = property_values(@widget['Object'])
35
+ primitives(values).merge(
36
+ key: widget_key, '$widgetId': widget_key, advanced: false,
37
+ datasource: datasource, columns: columns.map { compile_column(_1) },
38
+ class: css_class
39
+ )
40
+ end
41
+
42
+ def primitives(values)
43
+ values.each_with_object({}) do |(key, pair), result|
44
+ type, value = pair
45
+ compiled = primitive(type, value)
46
+ result[key.to_sym] = compiled unless compiled.equal?(:undefined)
47
+ end
48
+ end
49
+
50
+ def primitive(type, value) # rubocop:disable Metrics/CyclomaticComplexity
51
+ compiled = case type
52
+ when 'Boolean' then value['PrimitiveValue'] == 'true'
53
+ when 'Integer' then value['PrimitiveValue'].to_i
54
+ when 'Enumeration' then value['PrimitiveValue'].to_s
55
+ when 'TextTemplate' then expression(translated_text(value['TextTemplate']))
56
+ when 'Widgets' then [] if array(value['Widgets']).empty?
57
+ end
58
+ compiled.nil? ? :undefined : compiled
59
+ end # rubocop:enable Metrics/CyclomaticComplexity
60
+
61
+ def datasource
62
+ {
63
+ '$raw' => "DatabaseObjectListProperty(#{js_object(
64
+ dataSourceId: data_source_id, entity: entity_name,
65
+ operationId: WebOperationCompiler.operation_id(@page_name, @widget['Name']), sort: []
66
+ )})"
67
+ }
68
+ end
69
+
70
+ def compile_column(object) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
71
+ values = property_values(object)
72
+ mode = values.fetch('showContentAs').last['PrimitiveValue']
73
+ attribute = attribute_name(values)
74
+ entity, name = attribute.rpartition('.').then { |prefix, _, suffix| [prefix, suffix] }
75
+ result = primitives(values).merge(
76
+ showContentAs: mode, attribute: attribute.empty? ? :undefined : raw_attribute(entity, name),
77
+ dynamicText: :undefined, header: expression(text_value(values['header']&.last)),
78
+ tooltip: :undefined, filter: compile_widgets(values['filter']&.last),
79
+ visible: expression(true), exportValue: :undefined
80
+ )
81
+ result[:dynamicText] = dynamic_text(values, entity, name) if mode == 'dynamicText'
82
+ result[:content] = templated_content(values) if mode == 'customContent'
83
+ result.merge!(association_filter(values)) if association_filter?(values)
84
+ result
85
+ end # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
86
+
87
+ def association_filter?(values)
88
+ !association_steps(values).empty? && !filter_widget(values).nil?
89
+ end
90
+
91
+ def association_filter(values) # rubocop:disable Metrics/AbcSize
92
+ filter = filter_widget(values)
93
+ steps = association_steps(values)
94
+ steps.flat_map { [_1['Association'], _1['DestinationEntity']] }.join('/')
95
+ association = steps.first['Association'].to_s
96
+ endpoint = steps.last['DestinationEntity'].to_s
97
+ caption = attribute_name(values).split('.').last
98
+ selectable_id = "#{data_source_id}$#{filter['Name']}"
99
+ operation_id = WebOperationCompiler.operation_id(
100
+ @page_name, "#{@widget['Name']}$#{filter['Name']}"
101
+ )
102
+ {
103
+ filterAssociation: raw_property('ListAssociationProperty', {
104
+ type: filter_boolean(filter, 'multiSelect') ? 'ReferenceSet' : 'Reference',
105
+ entity: :undefined, path: '', attribute: association, endpointEntity: endpoint,
106
+ selectableObjectsId: selectable_id, filterable: true, dataSourceId: data_source_id
107
+ }),
108
+ filterAssociationOptions: raw_property('DatabaseObjectListProperty', {
109
+ dataSourceId: selectable_id, entity: endpoint, operationId: operation_id,
110
+ sort: [[caption, 'asc']]
111
+ }),
112
+ filterAssociationOptionLabel: raw_property('ListExpressionProperty', {
113
+ expression: {
114
+ expr: { type: 'variable', variable: 'currentObject', path: caption },
115
+ args: { currentObject: { widget: widget_key, source: 'object' } }
116
+ }, dataSourceId: selectable_id
117
+ })
118
+ }
119
+ end
120
+
121
+ def raw_property(name, config) = { '$raw' => "#{name}(#{js_object(config)})" }
122
+
123
+ def filter_widget(values)
124
+ array(values['filter']&.last&.fetch('Widgets', nil)).first
125
+ end
126
+
127
+ def association_steps(values)
128
+ array(values['attribute']&.last&.dig('AttributeRef', 'EntityRef', 'Steps'))
129
+ end
130
+
131
+ def filter_boolean(widget, key)
132
+ property_values(widget['Object'])[key]&.last&.fetch('PrimitiveValue', nil) == 'true'
133
+ end
134
+
135
+ def dynamic_text(values, entity, name)
136
+ template = values['dynamicText']&.last&.fetch('TextTemplate', nil)
137
+ parameters = array(template&.fetch('Parameters', nil))
138
+ return :undefined unless parameters.one? && !name.empty?
139
+
140
+ variable = { type: 'variable', variable: 'currentObject', path: name }
141
+ expr = if attribute_type(entity, name) == 'DateTime'
142
+ { type: 'function', name: '_format',
143
+ parameters: [variable, { type: 'literal', value: JSON.generate(type: 'datetime') }] }
144
+ else
145
+ variable
146
+ end
147
+ raw_list_expression(expr)
148
+ end
149
+
150
+ def raw_list_expression(expr)
151
+ payload = {
152
+ expression: { expr:, args: { currentObject: { widget: widget_key, source: 'object' } } },
153
+ dataSourceId: data_source_id
154
+ }
155
+ { '$raw' => "ListExpressionProperty(#{js_object(payload)})" }
156
+ end
157
+
158
+ def templated_content(values)
159
+ widgets = array(values['content']&.last&.fetch('Widgets', nil))
160
+ rendered = @render_widgets.call(widgets, widget_key, entity_name)
161
+ { '$raw' => "TemplatedWidgetProperty(#{js_object(
162
+ dataSourceId: data_source_id, editable: false,
163
+ children: { '$raw' => "() => #{rendered}" }
164
+ )})" }
165
+ end
166
+
167
+ def compile_widgets(value)
168
+ widgets = array(value&.fetch('Widgets', nil))
169
+ return [] if widgets.empty?
170
+
171
+ { '$raw' => @render_widgets.call(widgets, widget_key, entity_name) }
172
+ end
173
+
174
+ def raw_attribute(entity, name)
175
+ {
176
+ '$raw' => "AttributeProperty(#{js_object(
177
+ path: '', entity:, attribute: name, attributeType: attribute_type(entity, name),
178
+ sortable: true, filterable: true, dataSourceId: data_source_id, isList: false
179
+ )})"
180
+ }
181
+ end
182
+
183
+ def expression(value)
184
+ payload = { expression: { expr: { type: 'literal', value: }, args: {} } }
185
+ { '$raw' => "ExpressionProperty(#{js_object(payload)})" }
186
+ end
187
+
188
+ def js_object(value)
189
+ pairs = value.map { |key, item| "#{JSON.generate(key)}: #{js_value(item)}" }
190
+ "{ #{pairs.join(', ')} }"
191
+ end
192
+
193
+ def js_value(value)
194
+ return value['$raw'] if value.is_a?(Hash) && value.key?('$raw')
195
+ return 'undefined' if value == :undefined
196
+ return "[#{value.map { js_value(_1) }.join(', ')}]" if value.is_a?(Array)
197
+ return js_object(value) if value.is_a?(Hash)
198
+
199
+ JSON.generate(value)
200
+ end
201
+
202
+ def property_values(object)
203
+ array(object&.fetch('Properties', nil)).filter_map do |property|
204
+ type = @index[IO::BsonCodec.extract_id(property['TypePointer'])]
205
+ next unless type
206
+
207
+ [type.fetch('PropertyKey'), [type.dig('ValueType', 'Type'), property['Value']]]
208
+ end.to_h
209
+ end
210
+
211
+ def columns
212
+ value = property_values(@widget['Object']).fetch('columns').last
213
+ array(value['Objects'])
214
+ end
215
+
216
+ def supported_column?(object)
217
+ values = property_values(object)
218
+ mode = values['showContentAs']&.last&.fetch('PrimitiveValue', nil)
219
+ return attribute_name(values).include?('.') if %w[attribute dynamicText].include?(mode)
220
+ return false unless mode == 'customContent' && @render_widgets
221
+
222
+ array(values['content']&.last&.fetch('Widgets', nil)).any?
223
+ end
224
+
225
+ def attribute_name(values) = values['attribute']&.last&.dig('AttributeRef', 'Attribute').to_s
226
+
227
+ def xpath_source
228
+ property_values(@widget['Object'])['datasource']&.last&.fetch('DataSource', nil)
229
+ end
230
+
231
+ def entity_name = xpath_source.dig('EntityRef', 'Entity').to_s
232
+ def data_source_id = "p.#{Digest::SHA256.hexdigest(widget_key)[0, 6].to_i(16)}"
233
+ def widget_key = "p.#{@page_name}.#{@widget['Name']}"
234
+
235
+ def css_class
236
+ ["mx-name-#{@widget['Name']}", @widget.dig('Appearance', 'Class')]
237
+ .map(&:to_s).reject(&:empty?).uniq.join(' ')
238
+ end
239
+
240
+ def translated_text(text)
241
+ items = array(text&.dig('Template', 'Items'))
242
+ items.find { _1['LanguageCode'] == 'en_US' }&.fetch('Text', '') ||
243
+ items.first&.fetch('Text', '') || ''
244
+ end
245
+
246
+ def text_value(value) = translated_text(value&.fetch('TextTemplate', nil))
247
+
248
+ def attribute_type(entity, name) # rubocop:disable Metrics/AbcSize
249
+ document = @source.units_of('DomainModels$DomainModel').filter_map do |unit|
250
+ next unless unit.module_name == entity.split('.').first
251
+
252
+ array(unit.document['Entities']).find { _1['Name'] == entity.split('.').last }
253
+ end.first
254
+ attribute = array(document&.fetch('Attributes', nil)).find { _1['Name'] == name }
255
+ attribute_type_name(attribute&.fetch('NewType', nil))
256
+ end # rubocop:enable Metrics/AbcSize
257
+
258
+ def attribute_type_name(type)
259
+ type_name = type&.fetch('$Type', '').to_s.delete_prefix('DomainModels$')
260
+ type_name.delete_suffix('AttributeType').then { _1.empty? ? 'String' : _1 }
261
+ end
262
+
263
+ def widget_type
264
+ object_type_id = IO::BsonCodec.extract_id(@widget.dig('Object', 'TypePointer'))
265
+ return unless object_type_id
266
+
267
+ @index.values.find do |document|
268
+ IO::BsonCodec.extract_id(document.dig('ObjectType', '$ID')) == object_type_id
269
+ end
270
+ end
271
+
272
+ def document_index
273
+ return @source.document_index if @source.is_a?(SourceModel)
274
+
275
+ {}.tap { |index| @source.documents.each { index_document(_1, index) } }
276
+ end
277
+
278
+ def index_document(value, index)
279
+ case value
280
+ when Hash
281
+ id = IO::BsonCodec.extract_id(value['$ID'])
282
+ index[id] = value if id
283
+ value.each_value { index_document(_1, index) }
284
+ when Array then value.each { index_document(_1, index) }
285
+ end
286
+ end
287
+ end
288
+ # rubocop:enable Metrics
289
+ end
290
+ end
@@ -0,0 +1,280 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require 'json'
5
+
6
+ module Mxrb
7
+ module Compiler
8
+ # Mirrors Mendix's Database Connector build extension by lowering custom activities to Java calls.
9
+ # The mapping mirrors one cohesive external build-extension contract.
10
+ # rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity,
11
+ # rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
12
+ class DatabaseConnectorActionCompiler
13
+ include ModelValues
14
+
15
+ def initialize(source)
16
+ @source = source
17
+ @connections = connection_index
18
+ end
19
+
20
+ def compile(action)
21
+ connection, query = resolve_query(action['Query'].to_s)
22
+ java_action = java_action_for(query)
23
+ mappings = basic_mappings(action, connection, query, java_action)
24
+ mappings.concat(result_mappings(query, java_action))
25
+ {
26
+ '$ID' => action['$ID'], '$Type' => 'Microflows$JavaActionCallAction',
27
+ 'QueueSettings' => nil, 'ParameterMappings' => mappings,
28
+ 'ErrorHandlingType' => action['ErrorHandlingType'], 'JavaAction' => java_action,
29
+ 'ResultVariableName' => action['OutputVariableName'].to_s,
30
+ 'UseReturnVariable' => true
31
+ }
32
+ end
33
+
34
+ def unconfigured_write?(action)
35
+ return false unless action&.fetch('$Type', nil) == 'DatabaseConnector$ExecuteDatabaseQueryAction'
36
+
37
+ connection, query = resolve_query(action['Query'].to_s)
38
+ return false if selecting_query?(query)
39
+
40
+ override = array(action['ConnectionParameterMappings']).find do |mapping|
41
+ mapping['ParameterName'].to_s.casecmp('DBSource').zero?
42
+ end
43
+ return false unless override.nil? || override['Value'].to_s.strip.empty?
44
+
45
+ constant_name = connection['ConnectionString'].to_s
46
+ constant = @source.units.find do |unit|
47
+ unit.document['$Type'] == 'Constants$Constant' &&
48
+ "#{unit.module_name}.#{unit.document['Name']}" == constant_name
49
+ end
50
+ constant && constant.document['DefaultValue'].to_s.empty?
51
+ end
52
+
53
+ private
54
+
55
+ def connection_index
56
+ @source.units.each_with_object({}) do |unit, result|
57
+ next unless unit.document['$Type'] == 'DatabaseConnector$DatabaseConnection'
58
+
59
+ result["#{unit.module_name}.#{unit.document['Name']}"] = unit.document
60
+ end
61
+ end
62
+
63
+ def resolve_query(qualified)
64
+ connection_name, _, query_name = qualified.rpartition('.')
65
+ connection = @connections[connection_name]
66
+ raise CompilationError, "database connection #{connection_name.inspect} not found" unless connection
67
+
68
+ query = array(connection['Queries']).find { _1['Name'] == query_name }
69
+ raise CompilationError, "database query #{qualified.inspect} not found" unless query
70
+
71
+ [connection, query]
72
+ end
73
+
74
+ def java_action_for(query)
75
+ sql = remove_comments(query['Query'].to_s).downcase
76
+ selecting = selecting_sql?(sql)
77
+ mapped = array(query['TableMappings']).any?
78
+ if selecting
79
+ return mapped ? 'ExternalDatabaseConnector.ExecuteQuery' : 'ExternalDatabaseConnector.ExecuteStatement'
80
+ end
81
+ return statement_action(mapped) unless callable?(query, sql)
82
+ return 'ExternalDatabaseConnector.ExecuteCallable' if query['QueryType'].to_i == 3
83
+
84
+ if mapped
85
+ 'ExternalDatabaseConnector.ExecuteCallableQuery'
86
+ else
87
+ 'ExternalDatabaseConnector.ExecuteCallableStatement'
88
+ end
89
+ end
90
+
91
+ def selecting_query?(query)
92
+ selecting_sql?(remove_comments(query['Query'].to_s).downcase)
93
+ end
94
+
95
+ def selecting_sql?(sql)
96
+ (sql.start_with?('select ') && !sql.include?(' into ')) || sql.start_with?('with ')
97
+ end
98
+
99
+ def statement_action(mapped)
100
+ mapped ? 'ExternalDatabaseConnector.ExecuteQuery' : 'ExternalDatabaseConnector.ExecuteStatement'
101
+ end
102
+
103
+ def callable?(query, sql)
104
+ return true if query['QueryType'].to_i == 3
105
+ return false unless query['QueryType'].to_i.zero?
106
+
107
+ sql.match?(/\A\s*(?:\{\s*)?(?:call|exec(?:ute)?)\b/i)
108
+ end
109
+
110
+ def basic_mappings(action, connection, query, java_action)
111
+ values = {
112
+ 'connectionDetails' => expression_literal(connection_json(action, connection)),
113
+ 'sql' => expression_literal(runtime_query(action, query)),
114
+ 'queryParameters' => expression_literal(parameters_json(action, query))
115
+ }
116
+ values.map.with_index { |(name, value), index| parameter_mapping(action, java_action, name, value, index) }
117
+ end
118
+
119
+ def result_mappings(query, java_action)
120
+ return [] unless %w[
121
+ ExternalDatabaseConnector.ExecuteQuery ExternalDatabaseConnector.ExecuteCallableQuery
122
+ ExternalDatabaseConnector.ExecuteCallable
123
+ ].include?(java_action)
124
+
125
+ table = array(query['TableMappings']).first
126
+ return [] unless table
127
+
128
+ column_mapping = JSON.generate(
129
+ 'EntityName' => table['Entity'].to_s,
130
+ 'ColumnAttributeMapping' => array(table['Columns']).to_h do |column|
131
+ [column['ColumnName'].to_s, column['Attribute'].to_s.split('.').last]
132
+ end
133
+ )
134
+ offset = 10
135
+ [
136
+ parameter_mapping(query, java_action, 'columnMapping', expression_literal(column_mapping), offset),
137
+ entity_parameter_mapping(query, java_action, 'OutputEntity', table['Entity'].to_s, offset + 1)
138
+ ]
139
+ end
140
+
141
+ def connection_json(action, connection)
142
+ overrides = array(action['ConnectionParameterMappings']).to_h do |mapping|
143
+ [mapping['ParameterName'].to_s.downcase, mapping['Value'].to_s]
144
+ end
145
+ values = {
146
+ 'UserName' => raw(connection_value(overrides, connection, 'DBUsername', 'UserName')),
147
+ 'Password' => raw(connection_value(overrides, connection, 'DBPassword', 'Password')),
148
+ 'ConnectionString' => raw(connection_value(overrides, connection, 'DBSource', 'ConnectionString')),
149
+ 'DatabaseType' => connection['DatabaseType'].to_s,
150
+ 'AdditionalProperties' => additional_properties(overrides, connection)
151
+ }
152
+ raw_json(values)
153
+ end
154
+
155
+ def connection_value(overrides, connection, parameter, field)
156
+ expression = overrides[parameter.downcase]
157
+ expression ||= "@#{connection[field]}" unless connection[field].to_s.empty?
158
+ expression ||= "''"
159
+ "'+(#{expression})+'"
160
+ end
161
+
162
+ def additional_properties(overrides, connection)
163
+ array(connection['AdditionalProperties']).to_h do |property|
164
+ value = property['Value'] || {}
165
+ expression = overrides[property['Key'].to_s.downcase]
166
+ rendered = if expression
167
+ raw("'+(#{expression})+'")
168
+ elsif value['$Type'] == 'DatabaseConnector$ValueAsConstant'
169
+ raw("'+(@#{value['Value']})+'")
170
+ else
171
+ value['Value'].to_s
172
+ end
173
+ [property['Key'].to_s, rendered]
174
+ end
175
+ end
176
+
177
+ def runtime_query(action, query)
178
+ dynamic = action['DynamicQuery'].to_s
179
+ return "'+(#{dynamic})+'" unless dynamic.strip.empty?
180
+
181
+ sql = query['Query'].to_s
182
+ sql = query_builder_select(query) if sql.strip.empty?
183
+ sql.gsub("'", "''")
184
+ end
185
+
186
+ def query_builder_select(query)
187
+ table = array(query['TableMappings']).first
188
+ columns = table && array(table['Columns']).filter_map { valid_identifier(_1['ColumnName']) }
189
+ name = table && valid_identifier(table['TableName'])
190
+ unless name && columns.any?
191
+ raise CompilationError, 'database query builder has no executable table and column mapping'
192
+ end
193
+
194
+ "SELECT #{columns.join(', ')} FROM #{name}"
195
+ end
196
+
197
+ def valid_identifier(value)
198
+ identifier = value.to_s
199
+ return identifier if identifier.match?(/\A[A-Za-z_][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*)*\z/)
200
+
201
+ raise CompilationError, "unsafe database identifier #{identifier.inspect}"
202
+ end
203
+
204
+ def parameters_json(action, query)
205
+ mappings = array(action['ParameterMappings']).to_h do |mapping|
206
+ [mapping['ParameterName'].to_s.downcase, mapping['Value'].to_s]
207
+ end
208
+ result = {}
209
+ array(query['Parameters']).each_with_index do |parameter, index|
210
+ type = parameter.dig('DataType', '$Type').to_s.sub(/\ADataTypes\$/, '').sub(/Type\z/, '').upcase
211
+ value = parameter_value(type, mappings[parameter['ParameterName'].to_s.downcase])
212
+ result[(index + 1).to_s] = {
213
+ 'Name' => parameter['ParameterName'], 'DataType' => type,
214
+ 'SqlDataType' => parameter.dig('SqlDataType', 'DataTypeName').to_s,
215
+ 'Value' => value && raw(value), 'ParameterMode' => parameter['Mode'].to_s,
216
+ 'DatabaseParameterName' => parameter['DatabaseParameterName'].to_s,
217
+ 'ParameterEntityMapping' => nil
218
+ }
219
+ end
220
+ raw_json(result)
221
+ end
222
+
223
+ def parameter_value(type, expression)
224
+ case type
225
+ when 'DATETIME' then "'+(if (#{expression}) = NULL then NULL else dateTimeToEpoch(#{expression}))+'"
226
+ when 'BOOLEAN' then "'+toString(#{expression})+'"
227
+ when 'STRING'
228
+ "'+(if (#{expression}) = NULL then NULL else (if (#{expression}) = 'null' then " \
229
+ "'903be4ca-ed8b-ddcb-a339-741c4088484a' else (urlEncode(#{expression}))))+'"
230
+ when 'INTEGER', 'DECIMAL' then "'+(#{expression})+'"
231
+ else raise CompilationError, "unsupported database parameter type #{type.inspect}"
232
+ end
233
+ end
234
+
235
+ def expression_literal(value) = "'#{value}'"
236
+
237
+ def parameter_mapping(seed, java_action, name, value, index)
238
+ {
239
+ '$ID' => derived_id(seed, "mapping-#{index}"), '$Type' => 'Microflows$JavaActionParameterMapping',
240
+ 'Parameter' => "#{java_action}.#{name}",
241
+ 'Value' => {
242
+ '$ID' => derived_id(seed, "value-#{index}"),
243
+ '$Type' => 'Microflows$BasicCodeActionParameterValue', 'ValueExpression' => value
244
+ }
245
+ }
246
+ end
247
+
248
+ def entity_parameter_mapping(seed, java_action, name, entity, index)
249
+ {
250
+ '$ID' => derived_id(seed, "mapping-#{index}"), '$Type' => 'Microflows$JavaActionParameterMapping',
251
+ 'Parameter' => "#{java_action}.#{name}",
252
+ 'Value' => {
253
+ '$ID' => derived_id(seed, "value-#{index}"),
254
+ '$Type' => 'Microflows$EntityTypeCodeActionParameterValue',
255
+ 'Entity' => entity, 'ValueExpression' => "'#{entity}'"
256
+ }
257
+ }
258
+ end
259
+
260
+ def raw(value) = { '__mxrb_raw__' => value }
261
+
262
+ def raw_json(value)
263
+ rendered = JSON.generate(value)
264
+ rendered.gsub(/\{"__mxrb_raw__":"((?:\\.|[^"\\])*)"\}/) do
265
+ JSON.parse(%("#{Regexp.last_match(1)}"))
266
+ end
267
+ end
268
+
269
+ def remove_comments(sql) = sql.gsub(%r{/\*[\s\S]*?\*/|--.*}, '').strip
270
+
271
+ def derived_id(source, label)
272
+ seed = "#{IO::BsonCodec.extract_id(source['$ID'])}:database-connector:#{label}"
273
+ hex = Digest::SHA256.hexdigest(seed)[0, 32]
274
+ "#{hex[0, 8]}-#{hex[8, 4]}-4#{hex[13, 3]}-8#{hex[17, 3]}-#{hex[20, 12]}"
275
+ end
276
+ end
277
+ # rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity,
278
+ # rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
279
+ end
280
+ end