mendix-ruby-bridge 0.1.1

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 (54) hide show
  1. checksums.yaml +7 -0
  2. data/.mxcli-version +2 -0
  3. data/LICENSE +21 -0
  4. data/README.md +77 -0
  5. data/bin/git-mendix +4 -0
  6. data/bin/mendix-apply +145 -0
  7. data/bin/mendix-desktop +26 -0
  8. data/bin/mendix-git +121 -0
  9. data/bin/mendix-ruby +580 -0
  10. data/bin/mxcli +20 -0
  11. data/bin/setup-tools +25 -0
  12. data/lib/mendix_bridge/backend_server.rb +1404 -0
  13. data/lib/mendix_bridge/change_planner.rb +594 -0
  14. data/lib/mendix_bridge/config.rb +89 -0
  15. data/lib/mendix_bridge/dependency_index.rb +188 -0
  16. data/lib/mendix_bridge/desktop_app.rb +121 -0
  17. data/lib/mendix_bridge/document_parser.rb +120 -0
  18. data/lib/mendix_bridge/domain_parser.rb +103 -0
  19. data/lib/mendix_bridge/dsl.rb +540 -0
  20. data/lib/mendix_bridge/enumeration_parser.rb +32 -0
  21. data/lib/mendix_bridge/git_workflow.rb +321 -0
  22. data/lib/mendix_bridge/html_viewer.rb +672 -0
  23. data/lib/mendix_bridge/importer.rb +415 -0
  24. data/lib/mendix_bridge/inventory.rb +93 -0
  25. data/lib/mendix_bridge/mdl_generator.rb +343 -0
  26. data/lib/mendix_bridge/microflow_parser.rb +131 -0
  27. data/lib/mendix_bridge/migration.rb +290 -0
  28. data/lib/mendix_bridge/migration_executor.rb +234 -0
  29. data/lib/mendix_bridge/model.rb +204 -0
  30. data/lib/mendix_bridge/page_parser.rb +95 -0
  31. data/lib/mendix_bridge/presenter.rb +35 -0
  32. data/lib/mendix_bridge/project_creator.rb +181 -0
  33. data/lib/mendix_bridge/ruby_inventory_generator.rb +63 -0
  34. data/lib/mendix_bridge/security_parser.rb +72 -0
  35. data/lib/mendix_bridge/snapshot_diff.rb +58 -0
  36. data/lib/mendix_bridge/validator.rb +46 -0
  37. data/lib/mendix_bridge/version.rb +5 -0
  38. data/lib/mendix_bridge/visual_entity_plan.rb +176 -0
  39. data/lib/mendix_bridge.rb +127 -0
  40. data/share/applications/mendix-ruby-bridge.desktop +12 -0
  41. data/share/icons/hicolor/128x128/apps/mendix-ruby-bridge.png +0 -0
  42. data/share/icons/hicolor/256x256/apps/mendix-ruby-bridge.png +0 -0
  43. data/share/icons/hicolor/32x32/apps/mendix-ruby-bridge.png +0 -0
  44. data/share/icons/hicolor/48x48/apps/mendix-ruby-bridge.png +0 -0
  45. data/share/icons/hicolor/512x512/apps/mendix-ruby-bridge.png +0 -0
  46. data/share/icons/hicolor/64x64/apps/mendix-ruby-bridge.png +0 -0
  47. data/web/dist/apple-touch-icon.png +0 -0
  48. data/web/dist/assets/index-BO6iPT1P.css +1 -0
  49. data/web/dist/assets/index-JGODw81W.js +27 -0
  50. data/web/dist/brand/mendix-ruby-bridge.png +0 -0
  51. data/web/dist/favicon.png +0 -0
  52. data/web/dist/icons.svg +24 -0
  53. data/web/dist/index.html +17 -0
  54. metadata +179 -0
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MendixBridge
4
+ class MDLGenerator
5
+ TYPE_NAMES = {
6
+ "autonumber" => "AutoNumber",
7
+ "binary" => "Binary",
8
+ "boolean" => "Boolean",
9
+ "datetime" => "DateTime",
10
+ "decimal" => "Decimal",
11
+ "hash_string" => "HashedString",
12
+ "integer" => "Integer",
13
+ "long" => "Long",
14
+ "string" => "String"
15
+ }.freeze
16
+
17
+ def self.generate(
18
+ model,
19
+ skip_associations: [],
20
+ skip_module_roles: [],
21
+ skip_user_roles: [],
22
+ include_modules: false,
23
+ skip_modules: []
24
+ )
25
+ new(
26
+ model,
27
+ skip_associations:,
28
+ skip_module_roles:,
29
+ skip_user_roles:,
30
+ include_modules:,
31
+ skip_modules:
32
+ ).generate
33
+ end
34
+
35
+ def self.microflow_statement(app_module, microflow)
36
+ new(nil).send(:microflow_statement, app_module, microflow)
37
+ end
38
+
39
+ def self.page_statement(app_module, page)
40
+ new(nil).send(:page_statement, app_module, page)
41
+ end
42
+
43
+ def initialize(
44
+ model,
45
+ skip_associations: [],
46
+ skip_module_roles: [],
47
+ skip_user_roles: [],
48
+ include_modules: false,
49
+ skip_modules: []
50
+ )
51
+ @model = model
52
+ @skip_associations = skip_associations
53
+ @skip_module_roles = skip_module_roles
54
+ @skip_user_roles = skip_user_roles
55
+ @include_modules = include_modules
56
+ @skip_modules = skip_modules
57
+ end
58
+
59
+ def generate
60
+ statements = []
61
+ if @include_modules
62
+ statements.concat(@model.modules.filter_map do |app_module|
63
+ next if @skip_modules.include?(app_module.name)
64
+
65
+ "CREATE MODULE #{identifier(app_module.name)};"
66
+ end)
67
+ end
68
+ statements.concat(@model.modules.flat_map do |app_module|
69
+ enumerations = app_module.enumerations.map do |enumeration|
70
+ enumeration_statement(app_module, enumeration)
71
+ end
72
+ microflows = app_module.microflows.map do |microflow|
73
+ microflow_statement(app_module, microflow)
74
+ end
75
+ pages = app_module.pages.map { |page| page_statement(app_module, page) }
76
+ module_roles = app_module.module_roles.filter_map do |role|
77
+ name = "#{app_module.name}.#{role.name}"
78
+ module_role_statement(app_module, role) unless @skip_module_roles.include?(name)
79
+ end
80
+ entities = app_module.entities.map { |entity| entity_statement(app_module, entity) }
81
+ associations = app_module.entities.flat_map do |entity|
82
+ entity.associations.map do |association|
83
+ next if @skip_associations.include?(
84
+ "#{app_module.name}.#{association.name}"
85
+ )
86
+
87
+ association_statement(app_module, entity, association)
88
+ end.compact
89
+ end
90
+
91
+ module_roles + enumerations + entities + associations + microflows + pages
92
+ end)
93
+
94
+ statements.concat(user_role_statements)
95
+ statements.concat(project_security_statements)
96
+ statements.concat(navigation_statements)
97
+ statements.join("\n\n") << "\n"
98
+ end
99
+
100
+ private
101
+
102
+ def enumeration_statement(app_module, enumeration)
103
+ values = enumeration.values.map do |value|
104
+ " #{identifier(value.name)} '#{escape_string(value.caption)}'"
105
+ end
106
+ folder = if enumeration.folder
107
+ " FOLDER '#{escape_string(enumeration.folder)}'"
108
+ else
109
+ ""
110
+ end
111
+
112
+ "CREATE OR MODIFY ENUMERATION #{qualified(app_module.name, enumeration.name)} (\n" \
113
+ "#{values.join(",\n")}\n" \
114
+ ")#{folder};"
115
+ end
116
+
117
+ def microflow_statement(app_module, microflow)
118
+ name = qualified(app_module.name, microflow.name)
119
+ parameters = microflow.parameters.map do |parameter|
120
+ " $#{identifier(parameter.name)}: #{qualified_or_scalar(parameter.type)}"
121
+ end
122
+ returns = microflow.return_type ? "\nRETURNS #{qualified_or_scalar(microflow.return_type)}" : ""
123
+ folder = microflow.folder ? "\nFOLDER '#{escape_string(microflow.folder)}'" : ""
124
+ body = microflow.body.lines.map { |line| " #{line.rstrip}" }.join("\n")
125
+ grants = microflow.execute_roles.map do |role|
126
+ "GRANT EXECUTE ON MICROFLOW #{name} TO #{qualified_reference(role)};"
127
+ end
128
+
129
+ statement = "CREATE OR MODIFY MICROFLOW #{name} (\n" \
130
+ "#{parameters.join(",\n")}\n" \
131
+ ")#{returns}#{folder}\n" \
132
+ "BEGIN\n#{body}\nEND;"
133
+ grants.empty? ? statement : "#{statement}\n\n#{grants.join("\n")}"
134
+ end
135
+
136
+ def page_statement(app_module, page)
137
+ name = qualified(app_module.name, page.name)
138
+ settings = [
139
+ "Title: '#{escape_string(page.title)}'",
140
+ "Layout: #{qualified_reference(page.layout)}"
141
+ ]
142
+ settings << "Folder: '#{escape_string(page.folder)}'" if page.folder
143
+ unless page.parameters.empty?
144
+ parameters = page.parameters.map do |parameter|
145
+ "$#{identifier(parameter.name)}: #{qualified_reference(parameter.type)}"
146
+ end
147
+ settings << "Params: { #{parameters.join(', ')} }"
148
+ end
149
+ content = page.content.lines.map { |line| " #{line.rstrip}" }.join("\n")
150
+ grants = page.view_roles.map do |role|
151
+ "GRANT VIEW ON PAGE #{name} TO #{qualified_reference(role)};"
152
+ end
153
+
154
+ statement = "CREATE OR MODIFY PAGE #{name} (\n" \
155
+ " #{settings.join(",\n ")}\n" \
156
+ ") {\n#{content}\n}"
157
+ grants.empty? ? statement : "#{statement}\n\n#{grants.join("\n")}"
158
+ end
159
+
160
+ def module_role_statement(app_module, role)
161
+ statement = "CREATE MODULE ROLE #{qualified(app_module.name, role.name)}"
162
+ if role.description
163
+ statement << " DESCRIPTION '#{escape_string(role.description)}'"
164
+ end
165
+ "#{statement};"
166
+ end
167
+
168
+ def user_role_statements
169
+ @model.user_roles.filter_map do |role|
170
+ next if @skip_user_roles.include?(role.name)
171
+
172
+ management = role.manage_all_roles ? " MANAGE ALL ROLES" : ""
173
+ module_roles = role.module_roles.map { |name| qualified_reference(name) }
174
+ "CREATE USER ROLE #{identifier(role.name)} " \
175
+ "(#{module_roles.join(', ')})#{management};"
176
+ end
177
+ end
178
+
179
+ def project_security_statements
180
+ security = @model.project_security
181
+ return [] unless security
182
+
183
+ [
184
+ "ALTER PROJECT SECURITY LEVEL #{security.level.upcase};",
185
+ "ALTER PROJECT SECURITY DEMO USERS #{security.demo_users ? 'ON' : 'OFF'};"
186
+ ]
187
+ end
188
+
189
+ def navigation_statements
190
+ @model.navigation_profiles.map do |profile|
191
+ lines = [
192
+ "CREATE OR REPLACE NAVIGATION #{identifier(profile.name)}",
193
+ " HOME PAGE #{qualified_reference(profile.home_page)}"
194
+ ]
195
+ profile.role_home_pages.each do |entry|
196
+ lines << " HOME PAGE #{qualified_reference(entry.page)} " \
197
+ "FOR #{qualified_reference(entry.role)}"
198
+ end
199
+ if profile.login_page
200
+ lines << " LOGIN PAGE #{qualified_reference(profile.login_page)}"
201
+ end
202
+ if profile.not_found_page
203
+ lines << " NOT FOUND PAGE #{qualified_reference(profile.not_found_page)}"
204
+ end
205
+ unless profile.items.empty?
206
+ lines << " MENU ("
207
+ profile.items.each do |item|
208
+ lines.concat(navigation_item_lines(item, 4))
209
+ end
210
+ lines << " )"
211
+ end
212
+ "#{lines.join("\n")};"
213
+ end
214
+ end
215
+
216
+ def navigation_item_lines(item, indentation)
217
+ prefix = " " * indentation
218
+ if item.is_a?(Model::NavigationMenu)
219
+ lines = ["#{prefix}MENU '#{escape_string(item.caption)}' ("]
220
+ item.items.each do |child|
221
+ lines.concat(navigation_item_lines(child, indentation + 2))
222
+ end
223
+ lines << "#{prefix});"
224
+ lines
225
+ else
226
+ [
227
+ "#{prefix}MENU ITEM '#{escape_string(item.caption)}' " \
228
+ "#{item.action.upcase} #{qualified_reference(item.target)};"
229
+ ]
230
+ end
231
+ end
232
+
233
+ def entity_statement(app_module, entity)
234
+ persistence = entity.persistable ? "PERSISTENT" : "NON-PERSISTENT"
235
+ qualified_name = qualified(app_module.name, entity.name)
236
+
237
+ attributes = entity.attributes.map do |attribute|
238
+ " #{identifier(attribute.name)}: #{attribute_type(attribute)}" \
239
+ "#{constraints(attribute)}"
240
+ end
241
+
242
+ statement = "CREATE OR MODIFY #{persistence} ENTITY #{qualified_name} (\n" \
243
+ "#{attributes.join(",\n")}\n" \
244
+ ");"
245
+ grants = entity.access_rules.map do |rule|
246
+ entity_access_statement(qualified_name, rule)
247
+ end
248
+ grants.empty? ? statement : "#{statement}\n\n#{grants.join("\n")}"
249
+ end
250
+
251
+ def entity_access_statement(entity_name, rule)
252
+ permissions = []
253
+ permissions << "CREATE" if rule.create
254
+ permissions << "DELETE" if rule.delete
255
+ permissions << "READ #{access_attribute_list(rule.read)}" unless rule.read.empty?
256
+ permissions << "WRITE #{access_attribute_list(rule.write)}" unless rule.write.empty?
257
+ raise ValidationError, "entity access rule requires at least one permission" if permissions.empty?
258
+
259
+ xpath = rule.xpath ? " WHERE '#{escape_string(rule.xpath)}'" : ""
260
+ "GRANT #{qualified_reference(rule.role)} ON #{entity_name} " \
261
+ "(#{permissions.join(', ')})#{xpath};"
262
+ end
263
+
264
+ def access_attribute_list(value)
265
+ return "*" if value == "*"
266
+
267
+ "(#{value.map { |name| identifier(name) }.join(', ')})"
268
+ end
269
+
270
+ def association_statement(app_module, entity, association)
271
+ association_name = qualified(app_module.name, association.name)
272
+ source = qualified(app_module.name, entity.name)
273
+ target = qualified_reference(association.target)
274
+ type = association.cardinality == "many" ? "ReferenceSet" : "Reference"
275
+ owner = association.owner == "both" ? "Both" : "Default"
276
+
277
+ "CREATE ASSOCIATION #{association_name}\n" \
278
+ " FROM #{source}\n" \
279
+ " TO #{target}\n" \
280
+ " TYPE #{type}\n" \
281
+ " OWNER #{owner};"
282
+ end
283
+
284
+ def attribute_type(attribute)
285
+ if attribute.type == "enumeration"
286
+ return "Enumeration(#{qualified_reference(attribute.options.fetch(:enumeration))})"
287
+ end
288
+
289
+ if attribute.type == "string"
290
+ length = attribute.options[:length]
291
+ return "String(#{length || 'unlimited'})"
292
+ end
293
+
294
+ TYPE_NAMES.fetch(attribute.type) do
295
+ raise ValidationError, "cannot generate MDL type #{attribute.type.inspect}"
296
+ end
297
+ end
298
+
299
+ def constraints(attribute)
300
+ parts = []
301
+ parts << "NOT NULL" if attribute.required
302
+ parts << "DEFAULT #{literal(attribute.default)}" unless attribute.default.nil?
303
+ parts.empty? ? "" : " #{parts.join(' ')}"
304
+ end
305
+
306
+ def literal(value)
307
+ case value
308
+ when true then "TRUE"
309
+ when false then "FALSE"
310
+ when Numeric then value.to_s
311
+ when String then "'#{value.gsub("'", "''")}'"
312
+ else
313
+ raise ValidationError, "unsupported default value: #{value.inspect}"
314
+ end
315
+ end
316
+
317
+ def escape_string(value)
318
+ value.to_s.gsub("'", "''")
319
+ end
320
+
321
+ def qualified(module_name, item_name)
322
+ "#{identifier(module_name)}.#{identifier(item_name)}"
323
+ end
324
+
325
+ def qualified_reference(value)
326
+ parts = value.to_s.split(".", 2)
327
+ raise ValidationError, "expected qualified reference, got #{value.inspect}" if parts.length != 2
328
+
329
+ qualified(*parts)
330
+ end
331
+
332
+ def qualified_or_scalar(value)
333
+ value.to_s.include?(".") ? qualified_reference(value) : identifier(value)
334
+ end
335
+
336
+ def identifier(value)
337
+ string = value.to_s
338
+ return string if string.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
339
+
340
+ %("#{string.gsub('"', '""')}")
341
+ end
342
+ end
343
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MendixBridge
4
+ class MicroflowParser
5
+ class << self
6
+ def parse(description)
7
+ mdl = description.fetch("mdl")
8
+ header = mdl.match(
9
+ /\b(?:microflow|nanoflow)\s+(?<name>[\w.]+)\s*\((?<parameters>.*?)\)\s*(?:returns\s+(?<returns>\S+))?/im
10
+ )
11
+ body = mdl[/\nbegin\s*\n(?<body>.*)\nend;\s*(?:\n|$)/im, :body]
12
+
13
+ return { "mdl" => mdl, "parse_status" => "unsupported" } unless header && body
14
+
15
+ activities = parse_activities(body)
16
+ microflow_calls = calls(mdl, "microflow")
17
+ nanoflow_calls = calls(mdl, "nanoflow")
18
+ javascript_action_calls = calls(mdl, "javascript action")
19
+ {
20
+ "parameters" => parse_parameters(header[:parameters]),
21
+ "return_type" => header[:returns],
22
+ "folder" => mdl[/^folder\s+'([^']*)'/i, 1],
23
+ "variables" => activities.filter_map { |activity| activity["result_variable"] }.uniq,
24
+ "activities" => activities,
25
+ "calls" => all_calls(mdl),
26
+ "microflow_calls" => microflow_calls,
27
+ "nanoflow_calls" => nanoflow_calls,
28
+ "javascript_action_calls" => javascript_action_calls,
29
+ "execute_roles" => mdl.scan(
30
+ /^grant\s+execute\s+on\s+microflow\s+[\w.]+\s+to\s+([\w.]+)/i
31
+ ).flatten,
32
+ "mdl" => mdl,
33
+ "parse_status" => "parsed"
34
+ }.compact
35
+ end
36
+
37
+ private
38
+
39
+ def calls(mdl, kind)
40
+ mdl.scan(/\bcall\s+#{Regexp.escape(kind)}\s+([\w.]+)/i).flatten.uniq
41
+ end
42
+
43
+ def all_calls(mdl)
44
+ mdl.scan(
45
+ /\bcall\s+(?:microflow|nanoflow|javascript\s+action)\s+([\w.]+)/i
46
+ ).flatten.uniq
47
+ end
48
+
49
+ def parse_parameters(source)
50
+ source.lines.filter_map do |line|
51
+ match = line.strip.delete_suffix(",").match(/\A\$(\w+):\s*(.+)\z/)
52
+ { "name" => match[1], "type" => match[2] } if match
53
+ end
54
+ end
55
+
56
+ def parse_activities(body)
57
+ activities = []
58
+ buffer = +""
59
+
60
+ body.each_line do |line|
61
+ stripped = line.strip
62
+ next if stripped.empty? || stripped.start_with?("@")
63
+ next if %w[else].include?(stripped) || stripped.match?(/\Aend\s+if;\z/i)
64
+
65
+ buffer << " " unless buffer.empty?
66
+ buffer << stripped
67
+
68
+ if buffer.start_with?("if ") && stripped.match?(/\bthen\z/i)
69
+ expression = buffer.sub(/\Aif\s+/i, "").sub(/\s+then\z/i, "")
70
+ activities << activity("decision", buffer, "expression" => expression)
71
+ buffer.clear
72
+ elsif stripped.end_with?(";")
73
+ statement = buffer.delete_suffix(";")
74
+ kind = activity_kind(statement)
75
+ activities << activity(kind, statement, activity_metadata(kind, statement))
76
+ buffer.clear
77
+ end
78
+ end
79
+
80
+ activities
81
+ end
82
+
83
+ def activity_kind(statement)
84
+ case statement
85
+ when /\A(?:\$\w+\s*=\s*)?call\s+microflow\b/i then "microflow_call"
86
+ when /\A(?:\$\w+\s*=\s*)?call\s+nanoflow\b/i then "nanoflow_call"
87
+ when /\A(?:\$\w+\s*=\s*)?call\s+javascript\s+action\b/i then "javascript_action_call"
88
+ when /\Aretrieve\b/i then "retrieve"
89
+ when /\Achange\b/i then "change"
90
+ when /\Adelete\b/i then "delete"
91
+ when /\A(?:\$\w+\s*=\s*)?create\b/i then "create"
92
+ when /\Areturn\b/i then "return"
93
+ when /\Ashow\s+message\b/i then "show_message"
94
+ when /\Aclose\s+page\b/i then "close_page"
95
+ when /\Alog\b/i then "log"
96
+ else "statement"
97
+ end
98
+ end
99
+
100
+ def activity_metadata(kind, statement)
101
+ case kind
102
+ when "microflow_call", "nanoflow_call", "javascript_action_call"
103
+ call_kind = {
104
+ "microflow_call" => "microflow",
105
+ "nanoflow_call" => "nanoflow",
106
+ "javascript_action_call" => "javascript action"
107
+ }.fetch(kind)
108
+ {
109
+ "target" => statement[/\bcall\s+#{Regexp.escape(call_kind)}\s+([\w.]+)/i, 1],
110
+ "result_variable" => statement[/\A\$(\w+)\s*=/, 1]
111
+ }.compact
112
+ when "retrieve"
113
+ {
114
+ "result_variable" => statement[/\Aretrieve\s+\$(\w+)/i, 1],
115
+ "source" => statement[/\sfrom\s+(.+)\z/i, 1]
116
+ }.compact
117
+ when "create"
118
+ { "result_variable" => statement[/\A\$(\w+)\s*=/, 1] }.compact
119
+ when "return"
120
+ { "value" => statement.sub(/\Areturn\b\s*/i, "") }
121
+ else
122
+ {}
123
+ end
124
+ end
125
+
126
+ def activity(kind, statement, metadata = {})
127
+ { "kind" => kind, "statement" => statement, **metadata }
128
+ end
129
+ end
130
+ end
131
+ end