mxrb 0.1.3 → 0.1.5
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/bin/mxrb +455 -10
- data/docs/de-DE/README.md +1 -0
- data/docs/de-DE/oql-sql.md +42 -0
- data/docs/de-DE/project-structure.md +201 -2
- data/docs/de-DE/runtime-ruby.md +104 -0
- data/docs/de-DE/runtime-schema-migrations.md +44 -0
- data/docs/de-DE/validation-matrix.md +42 -4
- data/docs/en-US/README.md +1 -0
- data/docs/en-US/oql-sql.md +41 -0
- data/docs/en-US/project-structure.md +192 -2
- data/docs/en-US/runtime-ruby.md +100 -0
- data/docs/en-US/runtime-schema-migrations.md +41 -0
- data/docs/en-US/validation-matrix.md +39 -4
- data/docs/pt-BR/README.md +1 -0
- data/docs/pt-BR/oql-sql.md +41 -0
- data/docs/pt-BR/project-structure.md +197 -2
- data/docs/pt-BR/runtime-ruby.md +108 -0
- data/docs/pt-BR/runtime-schema-migrations.md +40 -0
- data/docs/pt-BR/validation-matrix.md +42 -5
- data/lib/mxrb/cli/help.rb +426 -0
- data/lib/mxrb/cli/release.rb +137 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
- data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
- data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
- data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
- data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
- data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
- data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
- data/lib/mxrb/domain_diagram.rb +409 -0
- data/lib/mxrb/dsl/builder.rb +43 -9
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +89 -15
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +14 -0
- data/lib/mxrb/io/bson_codec.rb +42 -1
- data/lib/mxrb/io/mpr_file.rb +146 -3
- data/lib/mxrb/model/attribute.rb +3 -0
- data/lib/mxrb/model/entity.rb +28 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/page.rb +257 -17
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -0
- data/lib/mxrb/oql/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +23 -1
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +2508 -0
- data/lib/mxrb/ruby_app/preset.rb +506 -0
- data/lib/mxrb/ruby_app/session_manager.rb +122 -0
- data/lib/mxrb/ruby_app.rb +1369 -0
- data/lib/mxrb/runtime/access_control.rb +408 -0
- data/lib/mxrb/runtime/native.rb +1015 -93
- data/lib/mxrb/runtime/scheduler.rb +396 -0
- data/lib/mxrb/runtime/schema_migrator.rb +598 -0
- data/lib/mxrb/runtime/shared_store.rb +335 -0
- data/lib/mxrb/runtime/sqlite_store.rb +729 -0
- data/lib/mxrb/scaffold/generator.rb +11 -3
- data/lib/mxrb/uml/activity_diagram.rb +118 -0
- data/lib/mxrb/uml/class_diagram.rb +118 -0
- data/lib/mxrb/uml/sequence_diagram.rb +112 -0
- data/lib/mxrb/uml/server.rb +146 -0
- data/lib/mxrb/uml/support.rb +35 -0
- data/lib/mxrb/version.rb +1 -1
- data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
- data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
- data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
- data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
- data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
- data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
- data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
- data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
- data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
- data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
- data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
- data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
- data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
- data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
- data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
- data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
- data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
- data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
- data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
- data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
- data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
- data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
- data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
- data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
- data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
- data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
- data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
- data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
- data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
- data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
- data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
- data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
- data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
- data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
- data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
- data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
- data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
- data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
- data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
- data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
- data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
- data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
- data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
- data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
- data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
- data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
- data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
- data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
- data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
- data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
- data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
- data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
- data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
- data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
- data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
- data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
- data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
- data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
- data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
- data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
- data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
- data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
- data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
- data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
- data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
- data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
- data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
- data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
- data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
- data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
- data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
- data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
- data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
- data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
- data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
- data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
- data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
- data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
- data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
- data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
- data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
- data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
- data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
- data/lib/mxrb/web_ui/domain.html +15 -0
- data/lib/mxrb/web_ui/modeler.html +16 -0
- data/lib/mxrb/web_ui/uml.html +31 -0
- data/lib/mxrb/web_ui.rb +89 -0
- data/lib/mxrb/widget_certification.rb +208 -0
- data/lib/mxrb/writer.rb +200 -24
- data/lib/mxrb.rb +26 -1
- metadata +156 -5
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'uri'
|
|
7
|
+
|
|
8
|
+
require_relative 'web_ui'
|
|
9
|
+
require_relative 'http/server'
|
|
10
|
+
|
|
11
|
+
module Mxrb
|
|
12
|
+
# Browser ER diagram for Mendix domain models. It edits visual layout only:
|
|
13
|
+
# entity locations plus local and cross-module connection anchors.
|
|
14
|
+
# rubocop:disable Metrics
|
|
15
|
+
module DomainDiagram
|
|
16
|
+
ANCHORS = %w[north east south west].freeze
|
|
17
|
+
ANCHOR_VALUES = {
|
|
18
|
+
'north' => '50;0', 'east' => '100;50',
|
|
19
|
+
'south' => '50;100', 'west' => '0;50'
|
|
20
|
+
}.freeze
|
|
21
|
+
VALUE_ANCHORS = ANCHOR_VALUES.invert.freeze
|
|
22
|
+
|
|
23
|
+
# Builds a framework-neutral JSON projection from arbitrary Mendix modules.
|
|
24
|
+
class Document
|
|
25
|
+
def initialize(path, modules: [])
|
|
26
|
+
@path = File.expand_path(path)
|
|
27
|
+
@selected_modules = Array(modules).map(&:to_s)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
Model::Project.open(@path).then do |project|
|
|
32
|
+
selected = project.modules
|
|
33
|
+
unless @selected_modules.empty?
|
|
34
|
+
unknown = @selected_modules.uniq - selected.map(&:name)
|
|
35
|
+
raise ArgumentError, "unknown modules: #{unknown.join(', ')}" unless unknown.empty?
|
|
36
|
+
|
|
37
|
+
selected = selected.select { @selected_modules.include?(_1.name) }
|
|
38
|
+
end
|
|
39
|
+
anchor_overrides = project.mpr.domain_diagram_anchors
|
|
40
|
+
known = project.modules.each_with_object({}) do |mod, names|
|
|
41
|
+
mod.entities.each do |entity|
|
|
42
|
+
qualified = entity.qualified_name || "#{mod.name}.#{entity.name}"
|
|
43
|
+
names[entity.id] = qualified
|
|
44
|
+
names[qualified] = qualified
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
{
|
|
48
|
+
project: { name: project.name, mendix_version: project.mendix_version },
|
|
49
|
+
module_filter_applied: !@selected_modules.empty?,
|
|
50
|
+
modules: selected.filter_map { module_payload(_1, known, anchor_overrides) }
|
|
51
|
+
}
|
|
52
|
+
ensure
|
|
53
|
+
project&.close
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def module_payload(mod, known, anchor_overrides = {})
|
|
60
|
+
domain = mod.domain_model
|
|
61
|
+
return unless domain
|
|
62
|
+
|
|
63
|
+
raw = mod.mpr.parse_contents(mod.mpr.unit(domain.id))
|
|
64
|
+
association_docs = array(raw['associations'] || raw['Associations']) +
|
|
65
|
+
array(raw['crossAssociations'] || raw['CrossAssociations'])
|
|
66
|
+
association_by_id = association_docs.to_h { [IO::BsonCodec.extract_id(_1['$ID']), _1] }
|
|
67
|
+
{
|
|
68
|
+
id: mod.id, name: mod.name,
|
|
69
|
+
entities: domain.entities.map { entity_payload(_1, mod.name) },
|
|
70
|
+
associations: domain.associations.filter_map do |association|
|
|
71
|
+
association_payload(
|
|
72
|
+
association, association_by_id[association.id] || {}, known, anchor_overrides
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def entity_payload(entity, module_name)
|
|
79
|
+
kind = if entity.respond_to?(:oql_view?) && entity.oql_view?
|
|
80
|
+
'oql_view'
|
|
81
|
+
elsif entity.persistable == true
|
|
82
|
+
'entity'
|
|
83
|
+
else
|
|
84
|
+
'dto'
|
|
85
|
+
end
|
|
86
|
+
{
|
|
87
|
+
id: entity.id, name: entity.name,
|
|
88
|
+
qualified_name: entity.qualified_name || "#{module_name}.#{entity.name}",
|
|
89
|
+
module: module_name, kind:,
|
|
90
|
+
persistent: entity.persistable == true,
|
|
91
|
+
x: Integer(entity.location&.fetch(:x, 0) || 0),
|
|
92
|
+
y: Integer(entity.location&.fetch(:y, 0) || 0),
|
|
93
|
+
attributes: entity.attributes.map do |attribute|
|
|
94
|
+
{
|
|
95
|
+
name: attribute.name, type: attribute.type.to_s,
|
|
96
|
+
required: attribute.required == true, key: attribute.unique == true
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def association_payload(association, raw, known, anchor_overrides = {})
|
|
103
|
+
from = known[association.from_entity_id]
|
|
104
|
+
to = known[association.to_entity_id]
|
|
105
|
+
return unless from && to
|
|
106
|
+
|
|
107
|
+
cross_module = raw['$Type'] == 'DomainModels$CrossAssociation'
|
|
108
|
+
override = anchor_overrides.fetch(association.id.to_s, {})
|
|
109
|
+
|
|
110
|
+
{
|
|
111
|
+
id: association.id, name: association.name, from:, to:,
|
|
112
|
+
type: association.association_type.to_s,
|
|
113
|
+
owner: association.owner.to_s,
|
|
114
|
+
source_anchor: override.fetch(
|
|
115
|
+
:source_anchor, anchor(raw['ParentConnection'], fallback: 'east')
|
|
116
|
+
),
|
|
117
|
+
target_anchor: override.fetch(
|
|
118
|
+
:target_anchor, anchor(raw['ChildConnection'], fallback: 'west')
|
|
119
|
+
),
|
|
120
|
+
cross_module:,
|
|
121
|
+
editable_anchors: true,
|
|
122
|
+
anchor_storage: cross_module ? 'mxrb' : 'native'
|
|
123
|
+
}
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def anchor(value, fallback:)
|
|
127
|
+
VALUE_ANCHORS.fetch(value.to_s, fallback)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def array(value) = IO::BsonCodec.parse_array(value)[:items]
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Applies only audited visual fields to a copied MPR.
|
|
134
|
+
class LayoutWriter
|
|
135
|
+
def initialize(path)
|
|
136
|
+
@path = File.expand_path(path)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def apply!(payload)
|
|
140
|
+
modules = Array(payload.fetch('modules'))
|
|
141
|
+
mpr = IO::MprFile.open(@path, readonly: false)
|
|
142
|
+
changed = 0
|
|
143
|
+
metadata_anchors = []
|
|
144
|
+
mpr.transaction do
|
|
145
|
+
module_units = mpr.units_by_containment('Modules').to_h do |unit|
|
|
146
|
+
[mpr.parse_contents(unit)['Name'].to_s, unit]
|
|
147
|
+
end
|
|
148
|
+
modules.each do |layout|
|
|
149
|
+
unit = module_units[layout.fetch('name').to_s]
|
|
150
|
+
raise ValidationError, "domain module #{layout['name']} not found" unless unit
|
|
151
|
+
|
|
152
|
+
module_changed, module_metadata = apply_module(mpr, unit, layout)
|
|
153
|
+
changed += module_changed
|
|
154
|
+
metadata_anchors.concat(module_metadata)
|
|
155
|
+
end
|
|
156
|
+
changed += mpr.write_domain_diagram_anchors(metadata_anchors)
|
|
157
|
+
end
|
|
158
|
+
changed
|
|
159
|
+
ensure
|
|
160
|
+
mpr&.close
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def apply_module(mpr, module_unit, layout)
|
|
166
|
+
domain_unit = mpr.children_of(module_unit.fetch('UnitID')).find do |unit|
|
|
167
|
+
unit['ContainmentName'] == 'DomainModel'
|
|
168
|
+
end
|
|
169
|
+
raise ValidationError, "domain model #{layout['name']} not found" unless domain_unit
|
|
170
|
+
|
|
171
|
+
doc = mpr.parse_contents(domain_unit)
|
|
172
|
+
changed = update_entities(doc, Array(layout['entities']))
|
|
173
|
+
association_changes, metadata_anchors = update_associations(
|
|
174
|
+
doc, Array(layout['associations'])
|
|
175
|
+
)
|
|
176
|
+
changed += association_changes
|
|
177
|
+
mpr.update_unit(domain_unit.fetch('UnitID'), doc) if changed.positive?
|
|
178
|
+
[changed, metadata_anchors]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def update_entities(doc, layouts)
|
|
182
|
+
by_id = layouts.to_h { [_1.fetch('id').to_s, _1] }
|
|
183
|
+
entities = array(doc['entities'] || doc['Entities'])
|
|
184
|
+
native_ids = entities.map { IO::BsonCodec.extract_id(_1['$ID']).to_s }
|
|
185
|
+
unknown = by_id.keys - native_ids
|
|
186
|
+
raise ValidationError, "unknown domain entities: #{unknown.join(', ')}" unless unknown.empty?
|
|
187
|
+
|
|
188
|
+
changed = entities.count do |entity|
|
|
189
|
+
layout = by_id[IO::BsonCodec.extract_id(entity['$ID']).to_s]
|
|
190
|
+
next false unless layout
|
|
191
|
+
|
|
192
|
+
x = bounded_integer(layout.fetch('x'), 'x')
|
|
193
|
+
y = bounded_integer(layout.fetch('y'), 'y')
|
|
194
|
+
key = entity.key?('Location') ? 'Location' : 'location'
|
|
195
|
+
value = entity[key].is_a?(Hash) ? { 'x' => x, 'y' => y } : "#{x};#{y}"
|
|
196
|
+
changed = entity[key] != value
|
|
197
|
+
entity[key] = value
|
|
198
|
+
changed
|
|
199
|
+
end
|
|
200
|
+
key = doc.key?('Entities') ? 'Entities' : 'entities'
|
|
201
|
+
doc[key] = IO::BsonCodec.build_array(entities)
|
|
202
|
+
changed
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def update_associations(doc, layouts)
|
|
206
|
+
by_id = layouts.to_h { [_1.fetch('id').to_s, _1] }
|
|
207
|
+
key = %w[associations Associations].find { doc.key?(_1) } || 'associations'
|
|
208
|
+
cross_key = %w[crossAssociations CrossAssociations].find { doc.key?(_1) } ||
|
|
209
|
+
'crossAssociations'
|
|
210
|
+
associations = array(doc[key])
|
|
211
|
+
cross_associations = array(doc[cross_key])
|
|
212
|
+
native_ids = associations.map { IO::BsonCodec.extract_id(_1['$ID']).to_s }
|
|
213
|
+
cross_ids = cross_associations.map { IO::BsonCodec.extract_id(_1['$ID']).to_s }
|
|
214
|
+
unknown = by_id.keys - native_ids - cross_ids
|
|
215
|
+
raise ValidationError, "unknown domain associations: #{unknown.join(', ')}" unless unknown.empty?
|
|
216
|
+
|
|
217
|
+
count = associations.count do |association|
|
|
218
|
+
layout = by_id[IO::BsonCodec.extract_id(association['$ID']).to_s]
|
|
219
|
+
next false unless layout
|
|
220
|
+
|
|
221
|
+
source = connection(layout.fetch('source_anchor'))
|
|
222
|
+
target = connection(layout.fetch('target_anchor'))
|
|
223
|
+
changed = association['ParentConnection'] != source || association['ChildConnection'] != target
|
|
224
|
+
association['ParentConnection'] = source
|
|
225
|
+
association['ChildConnection'] = target
|
|
226
|
+
changed
|
|
227
|
+
end
|
|
228
|
+
doc[key] = IO::BsonCodec.build_array(associations)
|
|
229
|
+
metadata = cross_ids.filter_map do |id|
|
|
230
|
+
layout = by_id[id]
|
|
231
|
+
next unless layout
|
|
232
|
+
|
|
233
|
+
{
|
|
234
|
+
id:, source_anchor: anchor_name(layout.fetch('source_anchor')),
|
|
235
|
+
target_anchor: anchor_name(layout.fetch('target_anchor'))
|
|
236
|
+
}
|
|
237
|
+
end
|
|
238
|
+
[count, metadata]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def bounded_integer(value, name)
|
|
242
|
+
number = Integer(value)
|
|
243
|
+
raise ValidationError, "#{name} is outside the diagram canvas" unless number.between?(-100_000, 100_000)
|
|
244
|
+
|
|
245
|
+
number
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def connection(anchor)
|
|
249
|
+
ANCHOR_VALUES.fetch(anchor_name(anchor))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def anchor_name(anchor)
|
|
253
|
+
ANCHOR_VALUES.fetch(anchor.to_s) do
|
|
254
|
+
raise ValidationError, "unknown association anchor #{anchor.inspect}"
|
|
255
|
+
end
|
|
256
|
+
anchor.to_s
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def array(value) = IO::BsonCodec.parse_array(value)[:items]
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Loopback-only server for the standalone editor.
|
|
263
|
+
class Server
|
|
264
|
+
MAX_BODY_BYTES = 2_097_152
|
|
265
|
+
LOOPBACK_HOSTS = %w[127.0.0.1 ::1 localhost].freeze
|
|
266
|
+
|
|
267
|
+
attr_reader :source, :output, :host, :port, :managed_paths
|
|
268
|
+
|
|
269
|
+
def initialize(source, output: nil, modules: [], host: '127.0.0.1', port: 4568, force: false,
|
|
270
|
+
reuse: false, lifecycle_token: nil)
|
|
271
|
+
@source = File.expand_path(source)
|
|
272
|
+
raise ArgumentError, "MPR not found: #{@source}" unless File.file?(@source)
|
|
273
|
+
|
|
274
|
+
@output = File.expand_path(output || default_output)
|
|
275
|
+
raise ArgumentError, 'output must be a safe copy, not the source MPR' if same_file_as_source?
|
|
276
|
+
raise ArgumentError, "output cannot be a directory: #{@output}" if File.directory?(@output)
|
|
277
|
+
raise ArgumentError, "output cannot be a symbolic link: #{@output}" if File.symlink?(@output)
|
|
278
|
+
|
|
279
|
+
@host = host.to_s
|
|
280
|
+
raise ArgumentError, 'domain model server must bind to loopback' unless LOOPBACK_HOSTS.include?(@host)
|
|
281
|
+
|
|
282
|
+
@port = Integer(port)
|
|
283
|
+
@modules = Array(modules)
|
|
284
|
+
@token = SecureRandom.hex(24)
|
|
285
|
+
@lifecycle_token = lifecycle_token
|
|
286
|
+
@managed_paths = [@output]
|
|
287
|
+
prepare_output(force:, reuse:)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def start
|
|
291
|
+
@server = Http::Server.new(host:, port:) { dispatch(_1, _2) }
|
|
292
|
+
@server.start { yield(_1) if block_given? }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def shutdown = @server&.shutdown
|
|
296
|
+
|
|
297
|
+
private
|
|
298
|
+
|
|
299
|
+
def default_output
|
|
300
|
+
File.join(File.dirname(source), "#{File.basename(source, '.mpr')}.domain-layout.mpr")
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def prepare_output(force:, reuse:)
|
|
304
|
+
if reuse
|
|
305
|
+
raise ArgumentError, "output not found: #{output}" unless File.file?(output)
|
|
306
|
+
|
|
307
|
+
return
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
raise ArgumentError, "output already exists: #{output}; use --force" if File.exist?(output) && !force
|
|
311
|
+
|
|
312
|
+
validate_external_contents_destination!
|
|
313
|
+
FileUtils.mkdir_p(File.dirname(output))
|
|
314
|
+
FileUtils.cp(source, output)
|
|
315
|
+
copy_external_contents
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def same_file_as_source?
|
|
319
|
+
source == output || (File.exist?(output) && File.identical?(source, output))
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def validate_external_contents_destination!
|
|
323
|
+
source_contents = File.join(File.dirname(source), 'mprcontents')
|
|
324
|
+
return unless File.directory?(source_contents)
|
|
325
|
+
|
|
326
|
+
output_directory = File.dirname(output)
|
|
327
|
+
return if File.expand_path(output_directory) == File.expand_path(File.dirname(source))
|
|
328
|
+
|
|
329
|
+
target = File.join(output_directory, 'mprcontents')
|
|
330
|
+
return unless File.exist?(target) || File.symlink?(target)
|
|
331
|
+
|
|
332
|
+
raise ArgumentError, "external contents destination already exists: #{target}"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def copy_external_contents
|
|
336
|
+
source_contents = File.join(File.dirname(source), 'mprcontents')
|
|
337
|
+
return unless File.directory?(source_contents)
|
|
338
|
+
|
|
339
|
+
output_directory = File.dirname(output)
|
|
340
|
+
return if File.expand_path(output_directory) == File.expand_path(File.dirname(source))
|
|
341
|
+
|
|
342
|
+
target = File.join(output_directory, 'mprcontents')
|
|
343
|
+
FileUtils.cp_r(source_contents, output_directory)
|
|
344
|
+
@managed_paths << target
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def dispatch(request, response)
|
|
348
|
+
if request.request_method == 'GET' && request.path == '/'
|
|
349
|
+
return render(response, 200, WebUi.page('domain'), 'text/html; charset=utf-8')
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
if request.request_method == 'GET' && request.path.start_with?('/assets/')
|
|
353
|
+
asset = WebUi.asset(request.path)
|
|
354
|
+
return render(response, 200, *asset) if asset
|
|
355
|
+
|
|
356
|
+
return json(response, 404, ok: false, error: 'not_found')
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
if request.request_method == 'GET' && request.path == '/api/diagram'
|
|
360
|
+
payload = Document.new(output, modules: @modules).to_h.merge(
|
|
361
|
+
output: output, csrf_token: @token
|
|
362
|
+
)
|
|
363
|
+
return json(response, 200, payload)
|
|
364
|
+
end
|
|
365
|
+
if request.path.start_with?('/api/admin/')
|
|
366
|
+
return json(response, 403, ok: false, error: 'forbidden') unless lifecycle_authorized?(request)
|
|
367
|
+
return json(response, 200, ok: true) if request.request_method == 'GET' && request.path == '/api/admin/status'
|
|
368
|
+
|
|
369
|
+
if request.request_method == 'POST' && request.path == '/api/admin/shutdown'
|
|
370
|
+
Thread.new { shutdown }
|
|
371
|
+
return json(response, 200, ok: true)
|
|
372
|
+
end
|
|
373
|
+
return json(response, 404, ok: false, error: 'not_found')
|
|
374
|
+
end
|
|
375
|
+
if request.request_method == 'POST' && request.path == '/api/layout'
|
|
376
|
+
raise ValidationError, 'invalid editor token' unless request['X-MXRB-Token'].to_s == @token
|
|
377
|
+
|
|
378
|
+
body = request.body.to_s
|
|
379
|
+
raise ValidationError, 'layout exceeds 2 MiB' if body.bytesize > MAX_BODY_BYTES
|
|
380
|
+
|
|
381
|
+
changed = LayoutWriter.new(output).apply!(JSON.parse(body))
|
|
382
|
+
return json(response, 200, ok: true, changed:, output:)
|
|
383
|
+
end
|
|
384
|
+
return json(response, 404, ok: false, error: 'not_found') if request.path.start_with?('/api/')
|
|
385
|
+
|
|
386
|
+
json(response, 404, ok: false, error: 'not_found')
|
|
387
|
+
rescue JSON::ParserError, ValidationError, KeyError, ArgumentError => e
|
|
388
|
+
json(response, 422, ok: false, error: e.message)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def lifecycle_authorized?(request)
|
|
392
|
+
@lifecycle_token && request['X-MXRB-Lifecycle-Token'].to_s == @lifecycle_token
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
def json(response, status, payload)
|
|
396
|
+
render(response, status, JSON.generate(payload), 'application/json; charset=utf-8')
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def render(response, status, body, type)
|
|
400
|
+
response.status = status
|
|
401
|
+
response['Content-Type'] = type
|
|
402
|
+
response['Cache-Control'] = 'no-store'
|
|
403
|
+
response['X-Content-Type-Options'] = 'nosniff'
|
|
404
|
+
response.body = body
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
end
|
|
408
|
+
# rubocop:enable Metrics
|
|
409
|
+
end
|
data/lib/mxrb/dsl/builder.rb
CHANGED
|
@@ -85,8 +85,16 @@ module Mxrb
|
|
|
85
85
|
_add_widget(:text, name, caption: caption || name.to_s, &block)
|
|
86
86
|
end
|
|
87
87
|
|
|
88
|
-
def data_grid(name, entity: nil, &block)
|
|
89
|
-
_add_widget(:data_grid, name, entity: entity, &block)
|
|
88
|
+
def data_grid(name, entity: nil, selection: nil, &block)
|
|
89
|
+
_add_widget(:data_grid, name, entity: entity, selection: selection, &block)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Gallery projections remain present beside an authoritative page
|
|
93
|
+
# deep_structure in exported projects. Accept the concise projection at
|
|
94
|
+
# every widget nesting level so arbitrary native pages can be evaluated
|
|
95
|
+
# and reconstructed from their lossless payload.
|
|
96
|
+
def gallery(name, entity: nil, &block)
|
|
97
|
+
_add_widget(:gallery, name, entity: entity, &block)
|
|
90
98
|
end
|
|
91
99
|
|
|
92
100
|
def tab_control(name, &block)
|
|
@@ -157,6 +165,8 @@ module Mxrb
|
|
|
157
165
|
|
|
158
166
|
# Builds sub-widgets inside a data_grid column sub-items or inside a container.
|
|
159
167
|
class WidgetBuilder
|
|
168
|
+
include WidgetDsl
|
|
169
|
+
|
|
160
170
|
def initialize(type, name, **options)
|
|
161
171
|
@type = type
|
|
162
172
|
@name = name.to_s
|
|
@@ -166,12 +176,18 @@ module Mxrb
|
|
|
166
176
|
@tabs = []
|
|
167
177
|
@search_bar = nil
|
|
168
178
|
@toolbar = nil
|
|
179
|
+
@children = []
|
|
180
|
+
@filters = []
|
|
169
181
|
end
|
|
170
182
|
|
|
171
|
-
def column(name, attribute: nil, caption: nil)
|
|
172
|
-
@columns << {
|
|
183
|
+
def column(name, attribute: nil, caption: nil, filter: nil)
|
|
184
|
+
@columns << {
|
|
185
|
+
name: name.to_s, attribute: attribute&.to_s, caption:, filter:
|
|
186
|
+
}.compact
|
|
173
187
|
end
|
|
174
188
|
|
|
189
|
+
def filter(widget) = @filters << widget
|
|
190
|
+
|
|
175
191
|
def tab_page(name, caption: nil, &block)
|
|
176
192
|
tab = TabPageBuilder.new(name, caption: caption)
|
|
177
193
|
tab.instance_eval(&block) if block
|
|
@@ -191,8 +207,8 @@ module Mxrb
|
|
|
191
207
|
end
|
|
192
208
|
|
|
193
209
|
%i[on_change on_click on_enter on_leave].each do |event|
|
|
194
|
-
define_method(event) do |microflow: nil, nanoflow: nil, action: nil|
|
|
195
|
-
choices = { microflow: microflow, nanoflow: nanoflow, action: action }.compact
|
|
210
|
+
define_method(event) do |microflow: nil, nanoflow: nil, page: nil, action: nil|
|
|
211
|
+
choices = { microflow: microflow, nanoflow: nanoflow, page: page, action: action }.compact
|
|
196
212
|
raise ArgumentError, "#{event} requires exactly one handler" unless choices.size == 1
|
|
197
213
|
@events << { event: event, kind: choices.keys.first, handler: choices.values.first.to_s }
|
|
198
214
|
end
|
|
@@ -204,8 +220,15 @@ module Mxrb
|
|
|
204
220
|
options[:tabs] = @tabs unless @tabs.empty?
|
|
205
221
|
options[:search_bar] = @search_bar if @search_bar
|
|
206
222
|
options[:toolbar] = @toolbar if @toolbar
|
|
207
|
-
|
|
223
|
+
options[:filters] = @filters unless @filters.empty?
|
|
224
|
+
value = { type: @type, name: @name, options: options, events: @events }
|
|
225
|
+
value[:children] = @children unless @children.empty?
|
|
226
|
+
value
|
|
208
227
|
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
def _widget_list = @children
|
|
209
232
|
end
|
|
210
233
|
|
|
211
234
|
class TabPageBuilder
|
|
@@ -315,6 +338,8 @@ module Mxrb
|
|
|
315
338
|
class Builder
|
|
316
339
|
include ConnectorDeclarations
|
|
317
340
|
|
|
341
|
+
attr_reader :path
|
|
342
|
+
|
|
318
343
|
def initialize(path)
|
|
319
344
|
@path = path
|
|
320
345
|
@mendix_version = "10.18.0"
|
|
@@ -325,6 +350,7 @@ module Mxrb
|
|
|
325
350
|
@native_units_path = nil
|
|
326
351
|
@native_unit_overrides = []
|
|
327
352
|
@project_assets = nil
|
|
353
|
+
@ruby_app_sources_path = nil
|
|
328
354
|
end
|
|
329
355
|
|
|
330
356
|
def mendix_version(v)
|
|
@@ -366,6 +392,10 @@ module Mxrb
|
|
|
366
392
|
}
|
|
367
393
|
end
|
|
368
394
|
|
|
395
|
+
def ruby_app_sources(path)
|
|
396
|
+
@ruby_app_sources_path = File.expand_path(path)
|
|
397
|
+
end
|
|
398
|
+
|
|
369
399
|
def native_unit(unit_id, container_id:, containment:, deep_structure:, module_name: nil)
|
|
370
400
|
raise ArgumentError, "deep_structure requires a Hash" unless deep_structure.is_a?(Hash)
|
|
371
401
|
|
|
@@ -421,6 +451,7 @@ module Mxrb
|
|
|
421
451
|
design_system: @design_system,
|
|
422
452
|
connectors: connector_requests.map(&:to_h),
|
|
423
453
|
project_assets: @project_assets,
|
|
454
|
+
ruby_app_sources_path: @ruby_app_sources_path,
|
|
424
455
|
native_units_path: @native_units_path,
|
|
425
456
|
native_unit_overrides: @native_unit_overrides
|
|
426
457
|
}
|
|
@@ -1247,8 +1278,8 @@ module Mxrb
|
|
|
1247
1278
|
end
|
|
1248
1279
|
|
|
1249
1280
|
%i[on_change on_click on_submit on_load].each do |event|
|
|
1250
|
-
define_method(event) do |target: nil, microflow: nil, nanoflow: nil, action: nil|
|
|
1251
|
-
choices = { microflow: microflow, nanoflow: nanoflow, action: action }.compact
|
|
1281
|
+
define_method(event) do |target: nil, microflow: nil, nanoflow: nil, page: nil, action: nil|
|
|
1282
|
+
choices = { microflow: microflow, nanoflow: nanoflow, page: page, action: action }.compact
|
|
1252
1283
|
raise ArgumentError, "#{event} requires exactly one handler" unless choices.size == 1
|
|
1253
1284
|
@events << {
|
|
1254
1285
|
event: event, target: target&.to_s,
|
|
@@ -1523,6 +1554,7 @@ module Mxrb
|
|
|
1523
1554
|
@return_expression = nil
|
|
1524
1555
|
@expected_body_fingerprint = nil
|
|
1525
1556
|
@allow_concurrent_execution = nil
|
|
1557
|
+
@apply_entity_access = nil
|
|
1526
1558
|
@mark_as_used = nil
|
|
1527
1559
|
@excluded = nil
|
|
1528
1560
|
end
|
|
@@ -1538,6 +1570,7 @@ module Mxrb
|
|
|
1538
1570
|
def return_type(t) = (@return_type = t.to_s)
|
|
1539
1571
|
def documentation(d) = (@doc = d)
|
|
1540
1572
|
def allow_concurrent_execution(value = true) = (@allow_concurrent_execution = !!value)
|
|
1573
|
+
def apply_entity_access(value = true) = (@apply_entity_access = !!value)
|
|
1541
1574
|
def mark_as_used(value = true) = (@mark_as_used = !!value)
|
|
1542
1575
|
def excluded(value = true) = (@excluded = !!value)
|
|
1543
1576
|
|
|
@@ -1603,6 +1636,7 @@ module Mxrb
|
|
|
1603
1636
|
body: @body, return_variable_name: @return_variable_name,
|
|
1604
1637
|
return_expression: @return_expression,
|
|
1605
1638
|
allow_concurrent_execution: @allow_concurrent_execution,
|
|
1639
|
+
apply_entity_access: @apply_entity_access,
|
|
1606
1640
|
mark_as_used: @mark_as_used, excluded: @excluded,
|
|
1607
1641
|
preserve_native_body: !@expected_body_fingerprint.nil? &&
|
|
1608
1642
|
@expected_body_fingerprint == current_fingerprint
|