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.
- checksums.yaml +4 -4
- data/README.de-DE.md +4 -0
- data/README.md +4 -0
- data/README.pt-BR.md +4 -0
- data/bin/mxrb +38 -2
- data/docs/de-DE/oql-sql.md +18 -0
- data/docs/de-DE/scaffolds.md +23 -0
- data/docs/en-US/oql-sql.md +18 -0
- data/docs/en-US/scaffolds.md +22 -0
- data/docs/pt-BR/entity-dsl.md +8 -0
- data/docs/pt-BR/oql-sql.md +18 -0
- data/docs/pt-BR/scaffolds.md +22 -0
- data/docs/pt-BR/writing.md +28 -0
- data/lib/mxrb/compare.rb +7 -0
- data/lib/mxrb/compiler/packager.rb +27 -13
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- data/lib/mxrb/dsl/builder.rb +100 -2
- data/lib/mxrb/exporter.rb +184 -32
- data/lib/mxrb/initializer.rb +1 -0
- data/lib/mxrb/integrity/validator.rb +18 -7
- data/lib/mxrb/model/entity.rb +17 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
- data/lib/mxrb/official_marketplace.rb +9 -3
- data/lib/mxrb/oql.rb +9 -3
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/runtime/clipboard.rb +62 -0
- data/lib/mxrb/runtime/database_workspace.rb +28 -0
- data/lib/mxrb/scaffold/cli.rb +20 -1
- data/lib/mxrb/scaffold/generator.rb +125 -3
- data/lib/mxrb/scaffold/help.rb +16 -2
- data/lib/mxrb/scaffold/recipes.rb +27 -0
- data/lib/mxrb/scaffold/templates.rb +11 -0
- data/lib/mxrb/scaffold/transaction.rb +2 -2
- data/lib/mxrb/team_server.rb +10 -7
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/writer.rb +94 -19
- data/lib/mxrb.rb +13 -7
- metadata +3 -1
data/lib/mxrb/dsl/builder.rb
CHANGED
|
@@ -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
|
-
|
|
67
|
-
|
|
68
|
-
@
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
120
|
+
def project_asset_files
|
|
110
121
|
source_root = File.dirname(@mpr_path)
|
|
111
|
-
|
|
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 =
|
|
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 =
|
|
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/
|
|
215
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
256
|
-
|
|
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
|
data/lib/mxrb/initializer.rb
CHANGED
|
@@ -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
|
-
@
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
data/lib/mxrb/model/entity.rb
CHANGED
|
@@ -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,
|