mxrb 0.1.1 → 0.1.3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.de-DE.md +4 -0
  3. data/README.md +4 -0
  4. data/README.pt-BR.md +4 -0
  5. data/bin/mxrb +38 -2
  6. data/docs/de-DE/oql-sql.md +18 -0
  7. data/docs/de-DE/scaffolds.md +23 -0
  8. data/docs/en-US/oql-sql.md +18 -0
  9. data/docs/en-US/scaffolds.md +22 -0
  10. data/docs/pt-BR/entity-dsl.md +8 -0
  11. data/docs/pt-BR/oql-sql.md +18 -0
  12. data/docs/pt-BR/scaffolds.md +22 -0
  13. data/docs/pt-BR/writing.md +28 -0
  14. data/lib/mxrb/compare.rb +7 -0
  15. data/lib/mxrb/compiler/packager.rb +27 -13
  16. data/lib/mxrb/compiler/portable_packager.rb +16 -11
  17. data/lib/mxrb/dsl/builder.rb +100 -2
  18. data/lib/mxrb/exporter.rb +184 -32
  19. data/lib/mxrb/initializer.rb +1 -0
  20. data/lib/mxrb/integrity/validator.rb +18 -7
  21. data/lib/mxrb/model/entity.rb +17 -1
  22. data/lib/mxrb/model/module.rb +71 -0
  23. data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
  24. data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
  25. data/lib/mxrb/official_marketplace.rb +9 -3
  26. data/lib/mxrb/oql.rb +9 -3
  27. data/lib/mxrb/progress.rb +242 -0
  28. data/lib/mxrb/runtime/clipboard.rb +62 -0
  29. data/lib/mxrb/runtime/database_workspace.rb +28 -0
  30. data/lib/mxrb/scaffold/cli.rb +20 -1
  31. data/lib/mxrb/scaffold/generator.rb +125 -3
  32. data/lib/mxrb/scaffold/help.rb +16 -2
  33. data/lib/mxrb/scaffold/recipes.rb +27 -0
  34. data/lib/mxrb/scaffold/templates.rb +11 -0
  35. data/lib/mxrb/scaffold/transaction.rb +2 -2
  36. data/lib/mxrb/team_server.rb +10 -7
  37. data/lib/mxrb/version.rb +1 -1
  38. data/lib/mxrb/writer.rb +94 -19
  39. data/lib/mxrb.rb +13 -7
  40. metadata +3 -1
@@ -6,6 +6,49 @@ require "base64"
6
6
 
7
7
  module Mxrb
8
8
  module Dsl
9
+ # Validates project-security references that cross DSL builders.
10
+ class SecurityValidator
11
+ def initialize(modules, security)
12
+ @modules = modules
13
+ @security = security
14
+ end
15
+
16
+ def validate!
17
+ return unless @security
18
+
19
+ errors = demo_users.flat_map { reference_errors(_1) }
20
+ duplicates = demo_users.group_by { _1.fetch(:name) }.select { |_name, users| users.size > 1 }
21
+ errors << "duplicate demo user name(s): #{duplicates.keys.join(', ')}" unless duplicates.empty?
22
+ return if errors.empty?
23
+
24
+ raise ValidationError, "security validation failed:\n- #{errors.join("\n- ")}"
25
+ end
26
+
27
+ private
28
+
29
+ def demo_users = Array(@security[:demo_users])
30
+
31
+ def reference_errors(user)
32
+ errors = []
33
+ missing_roles = user.fetch(:roles) - user_roles
34
+ errors << "demo user #{user[:name]} references missing user role(s): #{missing_roles.join(', ')}" unless
35
+ missing_roles.empty?
36
+ entity = user.fetch(:entity)
37
+ errors << "demo user #{user[:name]} references missing user entity #{entity}" unless
38
+ entity == "System.User" || entities.include?(entity)
39
+ errors
40
+ end
41
+
42
+ def user_roles = @security.fetch(:user_roles, []).map { _1.fetch(:name) }
43
+
44
+ def entities
45
+ @modules.flat_map do |mod|
46
+ definition = mod.to_h
47
+ definition.fetch(:entities, []).map { "#{definition.fetch(:name)}.#{_1.fetch(:name)}" }
48
+ end
49
+ end
50
+ end
51
+
9
52
  # Shared widget-building methods for PageBuilder and ContainerBuilder.
10
53
  # Includers must implement private `_widget_list` returning the target array.
11
54
  module WidgetDsl
@@ -361,9 +404,14 @@ module Mxrb
361
404
  end
362
405
 
363
406
  def validate!
407
+ validate_security!
364
408
  Architecture::Validator.new(graph).validate!
365
409
  end
366
410
 
411
+ def validate_security!
412
+ SecurityValidator.new(@modules, @security).validate!
413
+ end
414
+
367
415
  def definition
368
416
  {
369
417
  version: @mendix_version,
@@ -382,6 +430,8 @@ module Mxrb
382
430
  class SecurityBuilder
383
431
  def initialize
384
432
  @user_roles = []
433
+ @demo_users = []
434
+ @demo_users_declared = false
385
435
  @security_level = nil
386
436
  @admin_user_role = nil
387
437
  @demo_users_enabled = nil
@@ -414,6 +464,36 @@ module Mxrb
414
464
  @demo_users_enabled = enabled == true
415
465
  end
416
466
 
467
+ def demo_user(name, entity:, roles:, password:)
468
+ user_name = name.to_s
469
+ entity_name = entity.to_s
470
+ role_names = Array(roles).map(&:to_s).uniq
471
+ secret = password.to_s
472
+ raise ArgumentError, 'demo user name must not be empty' if user_name.empty?
473
+ raise ArgumentError, 'demo user name must not contain whitespace' unless user_name.match?(/\A[^\s]+\z/)
474
+ unless entity_name.match?(/\A[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9_]*\z/)
475
+ raise ArgumentError, 'demo user entity must be qualified as Module.Entity'
476
+ end
477
+ raise ArgumentError, 'demo user requires at least one role' if role_names.empty?
478
+ raise ArgumentError, 'demo user password must not be empty' if secret.empty?
479
+
480
+ @demo_users << {
481
+ name: user_name, entity: entity_name, roles: role_names, password: secret
482
+ }
483
+ @demo_users_declared = true
484
+ @demo_users_enabled = true
485
+ end
486
+
487
+ def evaluate(path)
488
+ instance_eval(File.read(path), path, 1)
489
+ end
490
+
491
+ def evaluate_dir(dir)
492
+ return unless File.directory?(dir)
493
+
494
+ Dir[File.join(dir, '*.rb')].sort.each { |path| evaluate(path) }
495
+ end
496
+
417
497
  def guest_access(enabled = true, role: nil)
418
498
  @guest_access_enabled = enabled == true
419
499
  @guest_user_role = role&.to_s
@@ -430,6 +510,7 @@ module Mxrb
430
510
  def to_h
431
511
  {
432
512
  user_roles: @user_roles,
513
+ demo_users: @demo_users_declared ? @demo_users : nil,
433
514
  security_level: @security_level,
434
515
  admin_user_role: @admin_user_role,
435
516
  demo_users_enabled: @demo_users_enabled,
@@ -791,15 +872,21 @@ module Mxrb
791
872
  @scheduled_events << sb.to_h
792
873
  end
793
874
 
794
- def native_document(name, type:, deep_structure:, containment: 'Documents')
875
+ def native_document(name, type:, deep_structure:, containment: 'Documents',
876
+ unit_id: nil, container_id: nil)
795
877
  raise ArgumentError, 'deep_structure requires a Hash' unless deep_structure.is_a?(Hash)
796
878
 
797
879
  @native_documents << {
798
880
  name: name.to_s, type: type.to_s, containment: containment.to_s,
881
+ unit_id: unit_id&.to_s, container_id: container_id&.to_s,
799
882
  doc: { '$Type' => type.to_s, 'Name' => name.to_s }.merge(deep_structure)
800
883
  }
801
884
  end
802
885
 
886
+ def bson_binary(base64, subtype: :generic)
887
+ BSON::Binary.new(Base64.strict_decode64(base64), subtype.to_sym)
888
+ end
889
+
803
890
  # Native responsive application shell. It deliberately uses only Mendix
804
891
  # core widgets, so generated projects do not depend on Atlas Core merely
805
892
  # to display their navigation.
@@ -989,6 +1076,7 @@ module Mxrb
989
1076
  @generalization = nil
990
1077
  @system_members = nil
991
1078
  @indexes = nil
1079
+ @oql_view = nil
992
1080
  end
993
1081
 
994
1082
  ATTR_TYPES.each do |type|
@@ -1000,6 +1088,16 @@ module Mxrb
1000
1088
  def non_persistent! = (@persistable = false)
1001
1089
  def documentation(d) = (@doc = d)
1002
1090
 
1091
+ # Marks an entity as an OQL-backed view. Modern Mendix projects keep the
1092
+ # query in a separate ViewEntitySourceDocument; older projects may keep
1093
+ # it directly on the embedded entity.
1094
+ def oql_view(source: nil, query: nil)
1095
+ raise ArgumentError, 'oql_view requires source or query' if source.nil? && query.nil?
1096
+
1097
+ @oql_view = { source: source&.to_s, query: query&.to_s }.compact
1098
+ @persistable = false
1099
+ end
1100
+
1003
1101
  def generalizes(entity)
1004
1102
  @generalization = entity.to_s
1005
1103
  end
@@ -1083,7 +1181,7 @@ module Mxrb
1083
1181
  name: @name, persistable: @persistable, documentation: @doc,
1084
1182
  attributes: @attributes, associations: @associations, lifecycle: @lifecycle,
1085
1183
  access_rules: @access_rules, generalization: @generalization,
1086
- system_members: @system_members, indexes: @indexes
1184
+ system_members: @system_members, indexes: @indexes, oql_view: @oql_view
1087
1185
  }
1088
1186
  end
1089
1187
 
data/lib/mxrb/exporter.rb CHANGED
@@ -9,6 +9,7 @@ module Mxrb
9
9
  # Exports an MPR into an editable, layered Ruby source tree.
10
10
  class Exporter
11
11
  LAYERS = %w[domain application presentation infrastructure].freeze
12
+ MODULE_PROGRESS_WEIGHT = 10
12
13
  MARKETPLACE_PROVENANCE = [
13
14
  ".mxrb/marketplace.lock.json",
14
15
  ".mxrb/marketplace",
@@ -63,22 +64,30 @@ module Mxrb
63
64
  end
64
65
 
65
66
  def export!(parallel: true)
66
- FileUtils.mkdir_p(@output_dir)
67
- Mxrb.open(@mpr_path) do |project|
68
- @architecture = project.architecture_definition
69
- export_app_structure
70
- export_project_assets
71
- export_native_units(project)
72
- export_security(project)
73
- export_architecture_contracts(project)
74
- mods = project.modules
75
- if parallel && mods.size > 1
76
- threads = mods.map { |mod| Thread.new { export_module(mod) } }
77
- threads.each(&:join)
78
- else
79
- mods.each { export_module(_1) }
67
+ Progress.with("Exporting #{File.basename(@mpr_path)}") do |progress|
68
+ FileUtils.mkdir_p(@output_dir)
69
+ Mxrb.open(@mpr_path) do |project|
70
+ @architecture = project.architecture_definition
71
+ units = project.all_units
72
+ modules = project.modules
73
+ assets = project_asset_files
74
+ progress.update(
75
+ current: 0,
76
+ total: 4 + assets.size + (units.size * 2) + (modules.size * MODULE_PROGRESS_WEIGHT),
77
+ detail: "preparing Ruby project"
78
+ )
79
+ export_app_structure
80
+ progress.advance(detail: "project structure")
81
+ export_project_assets(assets, progress)
82
+ export_native_units(project, units, progress)
83
+ export_security(project)
84
+ progress.advance(detail: "project security")
85
+ export_architecture_contracts(project)
86
+ progress.advance(detail: "navigation and design system")
87
+ export_modules(modules, progress, parallel:)
88
+ write_project(project)
89
+ progress.advance(detail: "project.rb")
80
90
  end
81
- write_project(project)
82
91
  end
83
92
  @output_dir
84
93
  end
@@ -91,6 +100,8 @@ module Mxrb
91
100
 
92
101
  export_domain(root, mod)
93
102
  export_microflows(root, mod)
103
+ export_application_documents(root, mod)
104
+ export_infrastructure_documents(root, mod)
94
105
  export_pages(root, mod)
95
106
  export_menus(root, mod)
96
107
  export_nanoflows(root, mod)
@@ -106,25 +117,31 @@ module Mxrb
106
117
  ].each { write(File.join(@output_dir, _1, ".keep"), "") }
107
118
  end
108
119
 
109
- def export_project_assets
120
+ def project_asset_files
110
121
  source_root = File.dirname(@mpr_path)
111
- files = (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
122
+ (Model::DesignSystem::ASSET_DIRECTORIES + MARKETPLACE_PROVENANCE).flat_map do |directory|
112
123
  root = File.join(source_root, directory)
113
124
  next [root] if File.file?(root) && !File.symlink?(root)
114
125
  next [] unless File.directory?(root) && !File.symlink?(root)
115
126
 
116
127
  Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH)
117
128
  end.select { File.file?(_1) && !File.symlink?(_1) }.sort
129
+ end
130
+
131
+ def export_project_assets(files, progress)
132
+ source_root = File.dirname(@mpr_path)
118
133
  entries = files.map do |source|
119
134
  relative = Pathname.new(source).relative_path_from(Pathname.new(source_root)).to_s
120
135
  target = File.join(@output_dir, relative)
121
136
  FileUtils.mkdir_p(File.dirname(target))
122
137
  FileUtils.cp(source, target)
123
- {
138
+ result = {
124
139
  "path" => relative,
125
140
  "size" => File.size(source),
126
141
  "sha256" => Digest::SHA256.file(source).hexdigest
127
142
  }
143
+ progress.advance(detail: "asset #{relative}")
144
+ result
128
145
  end
129
146
  write(
130
147
  File.join(@output_dir, ".mxrb", "assets.json"),
@@ -132,8 +149,8 @@ module Mxrb
132
149
  )
133
150
  end
134
151
 
135
- def export_native_units(project)
136
- units = project.all_units.filter_map do |unit|
152
+ def export_native_units(project, project_units, progress)
153
+ units = project_units.filter_map do |unit|
137
154
  doc = project.parse_bson(unit)
138
155
  type = doc["$Type"]
139
156
  next if type.to_s.empty? || type == "Projects$Project"
@@ -147,16 +164,21 @@ module Mxrb
147
164
  "type" => type,
148
165
  "contents" => Base64.strict_encode64(IO::BsonCodec.serialize(doc))
149
166
  }
167
+ ensure
168
+ progress.advance(detail: "native baseline")
150
169
  end
151
170
  write(
152
171
  File.join(@output_dir, ".mxrb", "native_units.json"),
153
172
  JSON.pretty_generate("format_version" => project.format_version.to_s, "units" => units)
154
173
  )
155
- source = project.all_units.filter_map do |unit|
174
+ source = project_units.filter_map do |unit|
156
175
  doc = project.parse_bson(unit)
157
176
  next if doc["$Type"].to_s.empty? || doc["$Type"] == "Projects$Project"
177
+ next if Model::Module::EDITABLE_DOCUMENT_TYPES.include?(doc["$Type"])
158
178
 
159
179
  native_unit_source(project, unit, doc)
180
+ ensure
181
+ progress.advance(detail: "editable native units")
160
182
  end
161
183
  write(
162
184
  File.join(@output_dir, ".mxrb", "native_units.rb"),
@@ -164,6 +186,18 @@ module Mxrb
164
186
  )
165
187
  end
166
188
 
189
+ def export_modules(modules, progress, parallel:)
190
+ operation = lambda do |mod|
191
+ export_module(mod)
192
+ progress.advance(MODULE_PROGRESS_WEIGHT, detail: "module #{mod.name}")
193
+ end
194
+ if parallel && modules.size > 1
195
+ modules.map { |mod| Thread.new { operation.call(mod) } }.each(&:join)
196
+ else
197
+ modules.each { operation.call(_1) }
198
+ end
199
+ end
200
+
167
201
  def native_unit_source(project, unit, doc)
168
202
  <<~RUBY
169
203
  native_unit #{ruby(unit.fetch("UnitID"))},
@@ -211,8 +245,10 @@ module Mxrb
211
245
 
212
246
  def export_module_scaffolding(root)
213
247
  %w[
214
- domain/enumerations domain/rules domain/policies
215
- application/ports/repositories application/queries application/validations application/jobs
248
+ domain/entities domain/dtos domain/oql_views domain/enumerations domain/constants
249
+ domain/rules domain/policies
250
+ application/ports/repositories application/queries application/queries/datasets
251
+ application/validations application/jobs application/jobs/scheduled_events
216
252
  presentation/features presentation/client_actions presentation/snippets presentation/view_models
217
253
  presentation/menus
218
254
  infrastructure/persistence/mendix infrastructure/persistence/external
@@ -238,26 +274,62 @@ module Mxrb
238
274
 
239
275
  def export_domain(root, mod)
240
276
  domain = File.join(root, "domain")
241
- entities_dir = File.join(domain, "entities")
242
- FileUtils.mkdir_p(entities_dir)
243
-
244
277
  associations = mod.associations.group_by(&:from_entity_id)
245
278
  architecture = architecture_module(mod.name)
246
- entity_files = unique_entity_filenames(mod.entities)
279
+ paths = {}
280
+ grouped = mod.entities.group_by { entity_domain_route(_1) }
281
+ grouped.each do |route, entities|
282
+ unique_entity_filenames(entities).each do |id, filename|
283
+ paths[id] = File.join(route, filename)
284
+ end
285
+ end
286
+ domain_documents = mod.domain_documents
287
+ oql_documents = mod.oql_view_documents
288
+ attached_oql_documents = {}
247
289
  mod.entities.each do |entity|
248
290
  entity_metadata = architecture&.fetch(:entities, [])&.find { _1[:name] == entity.name }
291
+ source = entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
292
+ if oql_view_entity?(entity) && (document = oql_document_for(entity, oql_documents, mod.name))
293
+ source = "#{source.rstrip}\n\n#{native_document_declaration(document)}"
294
+ attached_oql_documents[document.fetch(:id)] = true
295
+ end
249
296
  write(
250
- File.join(entities_dir, entity_files.fetch(entity.id)),
251
- entity_source(entity, mod, associations.fetch(entity.id, []), entity_metadata)
297
+ File.join(domain, paths.fetch(entity.id)), source
252
298
  )
253
299
  end
254
-
255
- loads = entity_files.values.sort.map do |filename|
256
- %(evaluate File.join(__dir__, "entities", #{ruby(filename)}))
300
+ used = paths.values.to_h { [_1, true] }
301
+ domain_documents.reject { attached_oql_documents[_1.fetch(:id)] }.each do |document|
302
+ relative = unique_relative_path(document.fetch(:route), underscore(document.fetch(:name)), used)
303
+ write(File.join(domain, relative), mapping_document_source(document))
304
+ paths[document.fetch(:id)] = relative
305
+ end
306
+ loads = paths.values.sort.map do |relative|
307
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
308
+ %(evaluate File.join(__dir__, #{segments}))
257
309
  end
258
310
  write(File.join(domain, "model.rb"), "#{loads.join("\n")}\n")
259
311
  end
260
312
 
313
+ def entity_domain_route(entity)
314
+ return 'oql_views' if oql_view_entity?(entity)
315
+ return 'dtos' unless entity.persistable
316
+
317
+ 'entities'
318
+ end
319
+
320
+ def oql_view_entity?(entity)
321
+ entity.respond_to?(:oql_view?) && entity.oql_view?
322
+ end
323
+
324
+ def oql_document_for(entity, documents, module_name)
325
+ reference = entity.oql_source_document.to_s
326
+ return nil if reference.empty?
327
+
328
+ documents.find do |document|
329
+ [document.fetch(:name), "#{module_name}.#{document.fetch(:name)}"].include?(reference)
330
+ end
331
+ end
332
+
261
333
  def export_microflows(root, mod)
262
334
  files = unique_filenames(mod.microflows)
263
335
  by_layer = { "application" => [], "infrastructure" => [] }
@@ -296,6 +368,74 @@ module Mxrb
296
368
  write_path_aggregator(File.join(presentation, "presentation.rb"), paths)
297
369
  end
298
370
 
371
+ def export_infrastructure_documents(root, mod)
372
+ documents = mod.infrastructure_documents
373
+ return if documents.empty?
374
+
375
+ infrastructure = File.join(root, "infrastructure")
376
+ used = {}
377
+ paths = documents.map do |document|
378
+ base = underscore(document.fetch(:name))
379
+ relative = unique_relative_path(document.fetch(:route), base, used)
380
+ write(File.join(infrastructure, relative), mapping_document_source(document))
381
+ relative
382
+ end
383
+ append_to_aggregator(File.join(infrastructure, "infrastructure.rb"), paths)
384
+ end
385
+
386
+ def export_application_documents(root, mod)
387
+ documents = mod.application_documents
388
+ return if documents.empty?
389
+
390
+ application = File.join(root, 'application')
391
+ used = {}
392
+ paths = documents.map do |document|
393
+ relative = unique_relative_path(document.fetch(:route), underscore(document.fetch(:name)), used)
394
+ write(File.join(application, relative), mapping_document_source(document))
395
+ relative
396
+ end
397
+ append_to_aggregator(File.join(application, 'application.rb'), paths)
398
+ end
399
+
400
+ def unique_relative_path(directory, base, used)
401
+ candidate = File.join(directory, "#{base}.rb")
402
+ suffix = 2
403
+ while used[candidate]
404
+ candidate = File.join(directory, "#{base}_#{suffix}.rb")
405
+ suffix += 1
406
+ end
407
+ used[candidate] = true
408
+ candidate
409
+ end
410
+
411
+ def mapping_document_source(document)
412
+ <<~RUBY
413
+ # frozen_string_literal: true
414
+
415
+ #{native_document_declaration(document)}
416
+ RUBY
417
+ end
418
+
419
+ def native_document_declaration(document)
420
+ <<~RUBY.rstrip
421
+ native_document #{symbol(document.fetch(:name))},
422
+ type: #{ruby(document.fetch(:type))},
423
+ unit_id: #{ruby(document.fetch(:id))},
424
+ container_id: #{ruby(document.fetch(:container_id))},
425
+ containment: #{ruby(document.fetch(:containment))},
426
+ deep_structure: #{native_ruby(document.fetch(:doc), 24)}
427
+ RUBY
428
+ end
429
+
430
+ def append_to_aggregator(path, relative_paths)
431
+ existing = File.exist?(path) ? File.read(path).lines : []
432
+ additions = relative_paths.sort.map do |relative|
433
+ segments = relative.split(File::SEPARATOR).map { ruby(_1) }.join(", ")
434
+ "evaluate File.join(__dir__, #{segments})\n"
435
+ end
436
+ write(path, (existing + additions).uniq.join)
437
+ end
438
+
299
439
  def export_menus(root, mod)
300
440
  menus = mod.menus
301
441
  return if menus.empty?
@@ -415,6 +555,12 @@ module Mxrb
415
555
  options = []
416
556
  options << " admin_user_role #{ruby(doc["AdminUserRole"])}" unless doc["AdminUserRole"].to_s.empty?
417
557
  options << " demo_users #{doc["EnableDemoUsers"] == true}"
558
+ demo_users = doc["DemoUsers"] || IO::BsonCodec.build_array([])
559
+ IO::BsonCodec.parse_array(demo_users).fetch(:items).each do |user|
560
+ assigned_roles = IO::BsonCodec.parse_array(user["UserRoles"]).fetch(:items)
561
+ options << " demo_user #{ruby(user["UserName"])}, entity: #{ruby(user["Entity"])}, " \
562
+ "roles: #{ruby(assigned_roles)}, password: #{ruby(user["Password"])}"
563
+ end
418
564
  guest = " guest_access #{doc["EnableGuestAccess"] == true}"
419
565
  guest += ", role: #{ruby(doc["GuestUserRole"])}" unless doc["GuestUserRole"].to_s.empty?
420
566
  options << guest
@@ -580,6 +726,12 @@ module Mxrb
580
726
  end
581
727
  flags = []
582
728
  flags << " non_persistent!" unless entity.persistable
729
+ if oql_view_entity?(entity)
730
+ options = []
731
+ options << "source: #{ruby(entity.oql_source_document)}" if entity.oql_source_document
732
+ options << "query: #{ruby(entity.oql_query)}" unless entity.oql_query.to_s.empty?
733
+ flags << " oql_view #{options.join(', ')}" unless options.empty?
734
+ end
583
735
  flags << " documentation #{ruby(entity.documentation)}" unless entity.documentation.to_s.empty?
584
736
  generalization = entity.respond_to?(:generalization_target) ? entity.generalization_target : nil
585
737
  flags << " generalizes #{ruby(generalization)}" if generalization
@@ -152,6 +152,7 @@ module Mxrb
152
152
  # frozen_string_literal: true
153
153
 
154
154
  require "dotenv/load"
155
+ Dotenv.load(File.join(__dir__, ".env"))
155
156
  require "mxrb"
156
157
 
157
158
  output = ENV.fetch("MXRB_OUTPUT_PATH", File.join(__dir__, "#{@mpr_name}"))
@@ -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,10 @@ module Mxrb
40
47
 
41
48
  def validate_units
42
49
  @units = @mpr.all_units
50
+ @progress.update(total: @units.size + 3, detail: "#{@units.size} units")
43
51
  validate_root
44
52
  validate_unit_ids
53
+ @progress.advance(detail: "unit tree")
45
54
  validate_contents
46
55
  end
47
56
 
@@ -78,6 +87,8 @@ module Mxrb
78
87
  next unless doc
79
88
 
80
89
  validate_doc_identity(unit, doc)
90
+ ensure
91
+ @progress&.advance(detail: "unit #{unit['UnitID']}")
81
92
  end
82
93
  end
83
94
 
@@ -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, :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"])
@@ -58,6 +61,19 @@ module Mxrb
58
61
  @generalization['generalization'] || @generalization['Generalization']
59
62
  end
60
63
 
64
+ def oql_view?
65
+ source_type = @source.is_a?(Hash) ? @source.fetch('$Type', '') : ''
66
+ @native_type.to_s.match?(/ViewEntity/i) ||
67
+ source_type.match?(/OqlViewEntitySource/i) ||
68
+ !@oql_query.to_s.empty?
69
+ end
70
+
71
+ def oql_source_document
72
+ return unless @source.is_a?(Hash)
73
+
74
+ @source['SourceDocument'] || @source['sourceDocument']
75
+ end
76
+
61
77
  def to_bson
62
78
  {
63
79
  "$ID" => @id,