mxrb 0.1.2 → 0.1.4
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 +458 -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/entity-dsl.md +8 -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/docs/pt-BR/writing.md +28 -0
- 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/packager.rb +27 -13
- data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
- data/lib/mxrb/compiler/portable_packager.rb +16 -11
- 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 +62 -11
- data/lib/mxrb/environment.rb +226 -0
- data/lib/mxrb/exporter.rb +265 -45
- data/lib/mxrb/http/server.rb +107 -0
- data/lib/mxrb/initializer.rb +80 -9
- data/lib/mxrb/integrity/validator.rb +32 -7
- 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 +44 -2
- data/lib/mxrb/model/microflow.rb +3 -1
- data/lib/mxrb/model/module.rb +71 -0
- data/lib/mxrb/model/page.rb +126 -15
- data/lib/mxrb/model/project.rb +3 -0
- data/lib/mxrb/modeler/catalog.rb +178 -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/reverse_translator.rb +394 -0
- data/lib/mxrb/oql/server.rb +4 -8
- data/lib/mxrb/oql.rb +32 -4
- data/lib/mxrb/progress.rb +242 -0
- data/lib/mxrb/project_lifecycle.rb +5 -2
- data/lib/mxrb/ruby_app/exporter.rb +1749 -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 +1361 -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/team_server.rb +10 -7
- 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 +278 -41
- data/lib/mxrb.rb +38 -8
- metadata +157 -5
data/lib/mxrb/runtime/native.rb
CHANGED
|
@@ -1,31 +1,75 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'securerandom'
|
|
4
|
+
require 'strscan'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'monitor'
|
|
7
|
+
require 'net/http'
|
|
8
|
+
require 'uri'
|
|
4
9
|
|
|
5
10
|
module Mxrb
|
|
6
11
|
module Runtime
|
|
7
12
|
module Native
|
|
8
13
|
# Graph interpretation intentionally keeps semantic dispatch together.
|
|
9
|
-
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
14
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
10
15
|
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
16
|
+
# rubocop:disable Metrics/ParameterLists
|
|
11
17
|
ObjectValue = Data.define(:entity, :id, :members)
|
|
12
18
|
|
|
13
19
|
# Transactional in-memory persistence used by the Ruby model Runtime.
|
|
14
20
|
class Store
|
|
15
|
-
|
|
21
|
+
LIFECYCLE_EVENTS = %i[
|
|
22
|
+
before_create after_create before_update after_update
|
|
23
|
+
before_commit after_commit before_delete after_delete
|
|
24
|
+
].freeze
|
|
25
|
+
|
|
26
|
+
def initialize(defaults: {}, hooks: {}, transient_entities: [])
|
|
16
27
|
@records = Hash.new { |records, entity| records[entity] = [] }
|
|
28
|
+
@defaults = defaults
|
|
29
|
+
@committed = {}
|
|
30
|
+
@hooks = hooks
|
|
31
|
+
@transient_entities = Array(transient_entities).map(&:to_s).to_h { [_1, true] }.freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def on(entity, event, &callback)
|
|
35
|
+
raise ArgumentError, "unknown lifecycle event #{event}" unless LIFECYCLE_EVENTS.include?(event.to_sym)
|
|
36
|
+
raise ArgumentError, 'lifecycle callback is required' unless callback
|
|
37
|
+
|
|
38
|
+
@hooks[entity.to_s] ||= {}
|
|
39
|
+
@hooks[entity.to_s][event.to_sym] ||= []
|
|
40
|
+
@hooks[entity.to_s][event.to_sym] << callback
|
|
41
|
+
self
|
|
17
42
|
end
|
|
18
43
|
|
|
19
|
-
def create(entity)
|
|
20
|
-
|
|
44
|
+
def create(entity, events: true)
|
|
45
|
+
raise ArgumentError, 'events must be boolean' unless [true, false].include?(events)
|
|
46
|
+
|
|
47
|
+
members = @defaults.fetch(entity, {}).dup
|
|
48
|
+
ObjectValue.new(entity:, id: SecureRandom.uuid, members:).tap do |object|
|
|
21
49
|
@records[entity] << object
|
|
22
50
|
end
|
|
23
51
|
end
|
|
24
52
|
|
|
25
53
|
def retrieve(entity) = @records[entity].dup
|
|
26
54
|
|
|
27
|
-
def
|
|
28
|
-
|
|
55
|
+
def find(entity, id) = @records[entity].find { _1.id == id.to_s }
|
|
56
|
+
|
|
57
|
+
def retrieve_association(association, start)
|
|
58
|
+
member = association.to_s.split('.').last
|
|
59
|
+
direct = Array(start&.members&.[](association) || start&.members&.[](member))
|
|
60
|
+
inverse = @records.values.flatten.select do |object|
|
|
61
|
+
Array(object.members[association] || object.members[member]).include?(start)
|
|
62
|
+
end
|
|
63
|
+
(direct + inverse).uniq
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def delete(value, events: true)
|
|
67
|
+
Array(value).each do |object|
|
|
68
|
+
run_hooks(:before_delete, object) if events
|
|
69
|
+
@records[object.entity].delete(object)
|
|
70
|
+
@committed.delete(record_key(object))
|
|
71
|
+
run_hooks(:after_delete, object) if events
|
|
72
|
+
end
|
|
29
73
|
end
|
|
30
74
|
|
|
31
75
|
def count(entity, predicate = nil)
|
|
@@ -34,43 +78,149 @@ module Mxrb
|
|
|
34
78
|
end
|
|
35
79
|
|
|
36
80
|
def snapshot
|
|
37
|
-
|
|
81
|
+
{
|
|
82
|
+
records: copy_records(@records),
|
|
83
|
+
committed: @committed.transform_values(&:dup)
|
|
84
|
+
}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def restore(snapshot)
|
|
88
|
+
@records.clear
|
|
89
|
+
snapshot.fetch(:records).each { |entity, values| @records[entity] = values }
|
|
90
|
+
@committed = snapshot.fetch(:committed).transform_values(&:dup)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def transaction
|
|
94
|
+
state = snapshot
|
|
95
|
+
result = yield
|
|
96
|
+
discard_uncommitted
|
|
97
|
+
result
|
|
98
|
+
rescue StandardError
|
|
99
|
+
restore(state)
|
|
100
|
+
raise
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def commit(value, events: true)
|
|
104
|
+
Array(value).each do |object|
|
|
105
|
+
creating = !@committed.key?(record_key(object))
|
|
106
|
+
run_hooks(creating ? :before_create : :before_update, object) if events
|
|
107
|
+
run_hooks(:before_commit, object) if events
|
|
108
|
+
@committed[record_key(object)] = object.members.dup
|
|
109
|
+
run_hooks(:after_commit, object) if events
|
|
110
|
+
run_hooks(creating ? :after_create : :after_update, object) if events
|
|
111
|
+
end
|
|
112
|
+
value
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def rollback(value)
|
|
116
|
+
Array(value).each do |object|
|
|
117
|
+
persisted = @committed[record_key(object)]
|
|
118
|
+
if persisted
|
|
119
|
+
object.members.replace(persisted.dup)
|
|
120
|
+
else
|
|
121
|
+
@records[object.entity].delete(object)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
value
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def close; end
|
|
128
|
+
|
|
129
|
+
private
|
|
130
|
+
|
|
131
|
+
def copy_records(records)
|
|
132
|
+
records.to_h do |entity, values|
|
|
38
133
|
[entity, values.map { ObjectValue.new(entity: _1.entity, id: _1.id, members: _1.members.dup) }]
|
|
39
134
|
end
|
|
40
135
|
end
|
|
41
136
|
|
|
42
|
-
|
|
137
|
+
# A root microflow is a unit of work. Persistent values only enter the
|
|
138
|
+
# durable record set through commit; returned uncommitted values remain
|
|
139
|
+
# valid Ruby objects, but are detached from subsequent retrieves.
|
|
140
|
+
# Non-persistent entities deliberately remain session-local in memory.
|
|
141
|
+
def discard_uncommitted
|
|
142
|
+
transient = @records.filter_map do |entity, values|
|
|
143
|
+
[entity, values.dup] if transient_entity?(entity)
|
|
144
|
+
end.to_h
|
|
43
145
|
@records.clear
|
|
44
|
-
|
|
146
|
+
transient.each { |entity, values| @records[entity] = values }
|
|
147
|
+
@committed.each do |(entity, id), members|
|
|
148
|
+
next if transient_entity?(entity)
|
|
149
|
+
|
|
150
|
+
@records[entity] << ObjectValue.new(entity:, id:, members: members.dup)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def transient_entity?(entity) = @transient_entities.key?(entity.to_s)
|
|
155
|
+
|
|
156
|
+
def record_key(object) = [object.entity, object.id]
|
|
157
|
+
|
|
158
|
+
def run_hooks(event, object)
|
|
159
|
+
raise ArgumentError, "unknown lifecycle event #{event}" unless LIFECYCLE_EVENTS.include?(event)
|
|
160
|
+
|
|
161
|
+
Array(@hooks.dig(object.entity, event)).each { _1.call(object) }
|
|
45
162
|
end
|
|
46
163
|
end
|
|
47
164
|
|
|
48
165
|
# Deliberately small Mendix-expression evaluator. Unsupported syntax is
|
|
49
166
|
# rejected rather than guessed, preserving deterministic test semantics.
|
|
50
167
|
class Expression
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
'>' => ->(left, right) { left > right },
|
|
55
|
-
'<' => ->(left, right) { left < right },
|
|
56
|
-
'>=' => ->(left, right) { left >= right },
|
|
57
|
-
'<=' => ->(left, right) { left <= right }
|
|
58
|
-
}.freeze
|
|
168
|
+
def initialize
|
|
169
|
+
@token_cache = {}
|
|
170
|
+
end
|
|
59
171
|
|
|
60
172
|
def evaluate(source, variables, node: nil)
|
|
61
|
-
text =
|
|
62
|
-
return nil if text.empty?
|
|
173
|
+
text = unwrap(source).strip
|
|
174
|
+
return nil if text.empty?
|
|
175
|
+
|
|
176
|
+
tokens = (@token_cache[text] ||= Lexer.new(text).tokens.freeze)
|
|
177
|
+
Parser.new(tokens, self, variables, node).parse
|
|
178
|
+
rescue ArgumentError, TypeError, KeyError
|
|
179
|
+
raise NativeRuntimeError, "unsupported Mendix expression: #{source.inspect}"
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def resolve_variable(text, variables)
|
|
183
|
+
name, member = text.delete_prefix('$').split('/', 2)
|
|
184
|
+
value = variables.fetch(name) do
|
|
185
|
+
raise NativeRuntimeError, "unknown variable $#{name}"
|
|
186
|
+
end
|
|
187
|
+
return value unless member
|
|
188
|
+
|
|
189
|
+
raise NativeRuntimeError, "$#{name} is not an object" unless value.is_a?(ObjectValue)
|
|
190
|
+
|
|
191
|
+
value.members[member.split('.').last]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def resolve_identifier(text, node)
|
|
63
195
|
return true if text.casecmp?('true')
|
|
64
196
|
return false if text.casecmp?('false')
|
|
65
|
-
return text
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
197
|
+
return nil if text.casecmp?('empty')
|
|
198
|
+
|
|
199
|
+
if node && bare_reference?(text)
|
|
200
|
+
member = text.split('/').last.split('.').last
|
|
201
|
+
return node.members[member] if node.members.key?(member)
|
|
202
|
+
end
|
|
203
|
+
return text if text.match?(/\A[A-Za-z_]\w*\.[A-Za-z_]\w*\.[A-Za-z_]\w*\z/)
|
|
69
204
|
return node_member(text, node) if node && bare_reference?(text)
|
|
70
205
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
206
|
+
raise ArgumentError, text
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def invoke(name, arguments)
|
|
210
|
+
case name.downcase
|
|
211
|
+
when 'tostring' then mendix_string(arguments.fetch(0))
|
|
212
|
+
when 'parseinteger' then Integer(arguments.fetch(0))
|
|
213
|
+
when 'parsedecimal' then Float(arguments.fetch(0))
|
|
214
|
+
when 'round' then arguments.fetch(0).round
|
|
215
|
+
when 'random' then Random.rand
|
|
216
|
+
when 'substring' then substring(*arguments)
|
|
217
|
+
when 'find' then arguments.fetch(0).to_s.index(arguments.fetch(1).to_s) || -1
|
|
218
|
+
when 'formatdatetime' then format_datetime(*arguments)
|
|
219
|
+
when 'contains' then arguments.fetch(0).to_s.include?(arguments.fetch(1).to_s)
|
|
220
|
+
when 'startswith' then arguments.fetch(0).to_s.start_with?(arguments.fetch(1).to_s)
|
|
221
|
+
when 'endswith' then arguments.fetch(0).to_s.end_with?(arguments.fetch(1).to_s)
|
|
222
|
+
else raise ArgumentError, name
|
|
223
|
+
end
|
|
74
224
|
end
|
|
75
225
|
|
|
76
226
|
private
|
|
@@ -79,15 +229,6 @@ module Mxrb
|
|
|
79
229
|
source.is_a?(Hash) ? (source['Value'] || source['Expression'] || '') : source.to_s
|
|
80
230
|
end
|
|
81
231
|
|
|
82
|
-
def quoted?(text)
|
|
83
|
-
text.length >= 2 && text.start_with?("'") && text.end_with?("'")
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
def strip_outer_parentheses(text)
|
|
87
|
-
text = text[1...-1].strip while text.start_with?('(') && matching_parenthesis(text, 0) == text.length - 1
|
|
88
|
-
text
|
|
89
|
-
end
|
|
90
|
-
|
|
91
232
|
def matching_parenthesis(text, opening)
|
|
92
233
|
depth = 0
|
|
93
234
|
quoted = false
|
|
@@ -104,104 +245,513 @@ module Mxrb
|
|
|
104
245
|
nil
|
|
105
246
|
end
|
|
106
247
|
|
|
107
|
-
def variable(text, variables)
|
|
108
|
-
name, member = text.delete_prefix('$').split('/', 2)
|
|
109
|
-
value = variables.fetch(name) do
|
|
110
|
-
raise NativeRuntimeError, "unknown variable $#{name}"
|
|
111
|
-
end
|
|
112
|
-
return value unless member
|
|
113
|
-
|
|
114
|
-
raise NativeRuntimeError, "$#{name} is not an object" unless value.is_a?(ObjectValue)
|
|
115
|
-
|
|
116
|
-
value.members[member.split('.').last]
|
|
117
|
-
end
|
|
118
|
-
|
|
119
248
|
# An attribute reference inside an XPath predicate, e.g. `Name` or
|
|
120
249
|
# `Clinic.Animal/Name`, resolved against the current candidate object.
|
|
121
250
|
def bare_reference?(text)
|
|
122
|
-
text.match?(%r{\A[A-Za-z_]\w*(?:/[A-Za-z_][\w.]*)?\z})
|
|
251
|
+
text.match?(%r{\A[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*(?:/[A-Za-z_][\w.]*)?\z})
|
|
123
252
|
end
|
|
124
253
|
|
|
125
254
|
def node_member(text, node)
|
|
126
255
|
node.members[text.split('/').last.split('.').last]
|
|
127
256
|
end
|
|
128
257
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
return
|
|
258
|
+
def mendix_string(value)
|
|
259
|
+
return 'true' if value == true
|
|
260
|
+
return 'false' if value == false
|
|
132
261
|
|
|
133
|
-
|
|
134
|
-
|
|
262
|
+
value.to_s
|
|
263
|
+
end
|
|
135
264
|
|
|
136
|
-
|
|
265
|
+
def substring(value, start, length = nil)
|
|
266
|
+
result = length.nil? ? value.to_s.slice(start..) : value.to_s.slice(start, length)
|
|
267
|
+
result.to_s
|
|
137
268
|
end
|
|
138
269
|
|
|
139
|
-
def
|
|
140
|
-
|
|
141
|
-
|
|
270
|
+
def format_datetime(value, pattern)
|
|
271
|
+
format = pattern.to_s.gsub('yyyy', '%Y').gsub('MMM', '%b').gsub('EEE', '%a')
|
|
272
|
+
.gsub('MM', '%m').gsub('dd', '%d').gsub('HH', '%H')
|
|
273
|
+
.gsub('mm', '%M').gsub('ss', '%S')
|
|
274
|
+
value.strftime(format)
|
|
275
|
+
end
|
|
142
276
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
277
|
+
# Recursive-descent parser for the expression subset used by native
|
|
278
|
+
# microflows. It deliberately accepts syntax, not arbitrary Ruby code.
|
|
279
|
+
class Parser
|
|
280
|
+
COMPARISONS = {
|
|
281
|
+
'=' => ->(left, right) { left == right },
|
|
282
|
+
'!=' => ->(left, right) { left != right },
|
|
283
|
+
'>' => ->(left, right) { left > right },
|
|
284
|
+
'<' => ->(left, right) { left < right },
|
|
285
|
+
'>=' => ->(left, right) { left >= right },
|
|
286
|
+
'<=' => ->(left, right) { left <= right }
|
|
287
|
+
}.freeze
|
|
288
|
+
|
|
289
|
+
def initialize(tokens, expression, variables, node)
|
|
290
|
+
@tokens = tokens
|
|
291
|
+
@index = 0
|
|
292
|
+
@expression = expression
|
|
293
|
+
@variables = variables
|
|
294
|
+
@node = node
|
|
295
|
+
advance
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def parse
|
|
299
|
+
value = parse_or
|
|
300
|
+
raise ArgumentError unless @token == :eof
|
|
301
|
+
|
|
302
|
+
value
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
private
|
|
306
|
+
|
|
307
|
+
def parse_or
|
|
308
|
+
left = parse_and
|
|
309
|
+
while accept_word('or')
|
|
310
|
+
right = parse_and
|
|
311
|
+
left = truthy?(left) || truthy?(right)
|
|
312
|
+
end
|
|
313
|
+
left
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def parse_and
|
|
317
|
+
left = parse_comparison
|
|
318
|
+
while accept_word('and')
|
|
319
|
+
right = parse_comparison
|
|
320
|
+
left = truthy?(left) && truthy?(right)
|
|
321
|
+
end
|
|
322
|
+
left
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def parse_comparison
|
|
326
|
+
left = parse_addition
|
|
327
|
+
while @token == :operator && COMPARISONS.key?(@value)
|
|
328
|
+
operator = consume(:operator)
|
|
329
|
+
left = COMPARISONS.fetch(operator).call(left, parse_addition)
|
|
330
|
+
end
|
|
331
|
+
left
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def parse_addition
|
|
335
|
+
left = parse_multiplication
|
|
336
|
+
while @token == :operator && %w[+ -].include?(@value)
|
|
337
|
+
operator = consume(:operator)
|
|
338
|
+
right = parse_multiplication
|
|
339
|
+
left = operator == '+' ? left + right : left - right
|
|
340
|
+
end
|
|
341
|
+
left
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def parse_multiplication
|
|
345
|
+
left = parse_unary
|
|
346
|
+
while @token == :operator && %w[* /].include?(@value)
|
|
347
|
+
operator = consume(:operator)
|
|
348
|
+
right = parse_unary
|
|
349
|
+
left = operator == '*' ? left * right : left / right
|
|
350
|
+
end
|
|
351
|
+
left
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def parse_unary
|
|
355
|
+
return !parse_unary if accept_word('not')
|
|
356
|
+
return -parse_unary if accept_operator('-')
|
|
357
|
+
|
|
358
|
+
parse_primary
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def parse_primary
|
|
362
|
+
return consume(@token) if %i[number string].include?(@token)
|
|
363
|
+
|
|
364
|
+
if @token == :datetime
|
|
365
|
+
consume(:datetime)
|
|
366
|
+
return Time.now
|
|
367
|
+
end
|
|
368
|
+
return @expression.resolve_variable(consume(:variable), @variables) if @token == :variable
|
|
369
|
+
return grouped if accept(:left_parenthesis)
|
|
370
|
+
return identifier if @token == :identifier
|
|
371
|
+
|
|
372
|
+
raise ArgumentError, @value.to_s
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def grouped
|
|
376
|
+
value = parse_or
|
|
377
|
+
consume(:right_parenthesis)
|
|
378
|
+
value
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
def identifier
|
|
382
|
+
name = consume(:identifier)
|
|
383
|
+
return @expression.resolve_identifier(name, @node) unless accept(:left_parenthesis)
|
|
384
|
+
|
|
385
|
+
arguments = []
|
|
386
|
+
unless accept(:right_parenthesis)
|
|
387
|
+
loop do
|
|
388
|
+
arguments << parse_or
|
|
389
|
+
break if accept(:right_parenthesis)
|
|
390
|
+
|
|
391
|
+
consume(:comma)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
@expression.invoke(name, arguments)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def accept_word(word)
|
|
398
|
+
return false unless @token == :identifier && @value.casecmp?(word)
|
|
399
|
+
|
|
400
|
+
advance
|
|
401
|
+
true
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def accept_operator(operator)
|
|
405
|
+
return false unless @token == :operator && @value == operator
|
|
406
|
+
|
|
407
|
+
advance
|
|
408
|
+
true
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def accept(token)
|
|
412
|
+
return false unless @token == token
|
|
413
|
+
|
|
414
|
+
advance
|
|
415
|
+
true
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def consume(token)
|
|
419
|
+
raise ArgumentError, "expected #{token}" unless @token == token
|
|
420
|
+
|
|
421
|
+
value = @value
|
|
422
|
+
advance
|
|
423
|
+
value
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def advance
|
|
427
|
+
@token, @value = @tokens[@index] || [:eof, nil]
|
|
428
|
+
@index += 1
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def truthy?(value)
|
|
432
|
+
value != false && !value.nil?
|
|
433
|
+
end
|
|
146
434
|
end
|
|
147
435
|
|
|
148
|
-
|
|
149
|
-
|
|
436
|
+
# Tokenization is cached by Expression so tight microflow loops do not
|
|
437
|
+
# repeatedly scan the same static Mendix source string.
|
|
438
|
+
class Lexer
|
|
439
|
+
def initialize(text)
|
|
440
|
+
@scanner = StringScanner.new(text)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def tokens
|
|
444
|
+
result = []
|
|
445
|
+
loop do
|
|
446
|
+
@scanner.skip(/\s+/)
|
|
447
|
+
token = next_token
|
|
448
|
+
result << token
|
|
449
|
+
return result if token.first == :eof
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
private
|
|
454
|
+
|
|
455
|
+
def next_token
|
|
456
|
+
return [:eof, nil] if @scanner.eos?
|
|
457
|
+
return [:datetime, nil] if @scanner.scan(/\[%CurrentDateTime%\]/i)
|
|
458
|
+
return [:string, scan_string] if @scanner.peek(1) == "'"
|
|
459
|
+
return [:number, number(@scanner.scan(/\d+(?:\.\d+)?/))] if @scanner.check(/\d/)
|
|
460
|
+
return [:variable, @scanner.scan(%r{\$[A-Za-z_]\w*(?:/[A-Za-z_][\w.]*)?})] if @scanner.check(/\$/)
|
|
461
|
+
return [:identifier, @scanner.scan(/[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*/)] if @scanner.check(/[A-Za-z_]/)
|
|
462
|
+
return [:operator, @scanner.scan(%r{!=|>=|<=|=|>|<|\+|-|\*|/})] if @scanner.check(%r{[!><=+*/-]})
|
|
463
|
+
|
|
464
|
+
punctuation
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def scan_string
|
|
468
|
+
@scanner.getch
|
|
469
|
+
value = +''
|
|
470
|
+
until @scanner.eos?
|
|
471
|
+
character = @scanner.getch
|
|
472
|
+
unless character == "'"
|
|
473
|
+
value << character
|
|
474
|
+
next
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
return value unless @scanner.peek(1) == "'"
|
|
478
|
+
|
|
479
|
+
@scanner.getch
|
|
480
|
+
value << "'"
|
|
481
|
+
end
|
|
482
|
+
raise ArgumentError, 'unterminated string'
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
def number(value)
|
|
486
|
+
value.include?('.') ? Float(value) : Integer(value)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def punctuation
|
|
490
|
+
character = @scanner.getch
|
|
491
|
+
token = { '(' => :left_parenthesis, ')' => :right_parenthesis, ',' => :comma }[character]
|
|
492
|
+
raise ArgumentError, character unless token
|
|
493
|
+
|
|
494
|
+
[token, character]
|
|
495
|
+
end
|
|
150
496
|
end
|
|
151
497
|
end
|
|
152
498
|
|
|
153
499
|
# Executes the graph and core data activities directly from the MPR.
|
|
154
500
|
class Interpreter
|
|
155
|
-
attr_reader :store, :log
|
|
501
|
+
attr_reader :store, :log, :effects
|
|
156
502
|
|
|
157
|
-
def initialize(project, store:
|
|
503
|
+
def initialize(project, store: nil, adapters: {}, java_custom_actions: {}, http: nil, policy: nil)
|
|
158
504
|
@project = project
|
|
159
|
-
@store = store
|
|
160
505
|
@expression = Expression.new
|
|
161
506
|
@log = []
|
|
507
|
+
@effects = []
|
|
508
|
+
@call_depth = 0
|
|
509
|
+
@call_monitor = Monitor.new
|
|
510
|
+
@collection_plans = {
|
|
511
|
+
true => {}.compare_by_identity,
|
|
512
|
+
false => {}.compare_by_identity
|
|
513
|
+
}
|
|
514
|
+
@identifier_cache = {}.compare_by_identity
|
|
515
|
+
@flow_case_cache = {}.compare_by_identity
|
|
516
|
+
@adapters = adapters.transform_keys(&:to_sym)
|
|
517
|
+
@java_custom_actions = java_custom_actions.to_h.transform_keys(&:to_s).freeze
|
|
518
|
+
@java_action_parameter_names = {}
|
|
519
|
+
@http = http || method(:http_request)
|
|
520
|
+
@policy = policy
|
|
521
|
+
@security_context = nil
|
|
522
|
+
@apply_entity_access = false
|
|
162
523
|
@flows = project.modules.flat_map do |mod|
|
|
163
524
|
mod.microflows.map { ["#{mod.name}.#{_1.name}", _1] }
|
|
164
525
|
end.to_h
|
|
526
|
+
entity_names = project.modules.flat_map do |mod|
|
|
527
|
+
mod.entities.map { [_1.id.to_s, "#{mod.name}.#{_1.name}"] }
|
|
528
|
+
end.to_h
|
|
529
|
+
defaults = project.modules.flat_map do |mod|
|
|
530
|
+
mod.entities.map do |entity|
|
|
531
|
+
["#{mod.name}.#{entity.name}", entity.attributes.to_h do |attribute|
|
|
532
|
+
[attribute.name, attribute_default(attribute)]
|
|
533
|
+
end]
|
|
534
|
+
end
|
|
535
|
+
end.to_h
|
|
536
|
+
transient_entities = project.modules.flat_map do |mod|
|
|
537
|
+
mod.entities.select { _1.persistable == false }.map { "#{mod.name}.#{_1.name}" }
|
|
538
|
+
end
|
|
539
|
+
@store = store || Store.new(defaults:, transient_entities:)
|
|
540
|
+
register_model_lifecycle
|
|
541
|
+
@associations = project.modules.flat_map do |mod|
|
|
542
|
+
mod.associations.map do |association|
|
|
543
|
+
["#{mod.name}.#{association.name}", {
|
|
544
|
+
type: association.association_type,
|
|
545
|
+
from: entity_names[association.from_entity_id.to_s]
|
|
546
|
+
}]
|
|
547
|
+
end
|
|
548
|
+
end.to_h
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def call(name, arguments = nil, context: nil, **keyword_arguments)
|
|
552
|
+
(@call_monitor ||= Monitor.new).synchronize do
|
|
553
|
+
execute_call(name, arguments, context:, **keyword_arguments)
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def clear_effects! = @effects.clear
|
|
558
|
+
|
|
559
|
+
# Counts stored objects of an entity, optionally narrowed by an XPath
|
|
560
|
+
# constraint. Used by the functional Executor's count expectations.
|
|
561
|
+
def count(entity, xpath = nil)
|
|
562
|
+
filter_by_xpath(store.retrieve(entity.to_s), xpath.to_s, {}).size
|
|
165
563
|
end
|
|
166
564
|
|
|
167
|
-
|
|
565
|
+
private
|
|
566
|
+
|
|
567
|
+
def execute_call(name, arguments = nil, context: nil, **keyword_arguments)
|
|
568
|
+
arguments = keyword_arguments if arguments.nil?
|
|
569
|
+
arguments = arguments.to_h.merge(keyword_arguments) unless keyword_arguments.empty?
|
|
570
|
+
previous_context = @security_context
|
|
571
|
+
previous_apply_entity_access = @apply_entity_access
|
|
572
|
+
root_call = @call_depth.zero?
|
|
573
|
+
@effects = [] if root_call
|
|
574
|
+
@call_depth += 1
|
|
168
575
|
flow = @flows[name]
|
|
169
576
|
raise NativeRuntimeError, "microflow #{name} not found" unless flow
|
|
170
577
|
|
|
171
|
-
|
|
172
|
-
|
|
578
|
+
@security_context = context unless context.nil?
|
|
579
|
+
@apply_entity_access = flow.respond_to?(:apply_entity_access) && flow.apply_entity_access == true
|
|
580
|
+
normalized = normalize_arguments(flow, arguments)
|
|
581
|
+
if root_call && store.respond_to?(:transaction)
|
|
582
|
+
store.transaction { execute(flow, normalized) }
|
|
583
|
+
else
|
|
584
|
+
snapshot = store.snapshot if root_call
|
|
585
|
+
execute(flow, normalized)
|
|
586
|
+
end
|
|
173
587
|
rescue StandardError
|
|
174
588
|
store.restore(snapshot) if snapshot
|
|
175
589
|
raise
|
|
590
|
+
ensure
|
|
591
|
+
@security_context = previous_context
|
|
592
|
+
@apply_entity_access = previous_apply_entity_access
|
|
593
|
+
@call_depth -= 1
|
|
176
594
|
end
|
|
177
595
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
596
|
+
def attribute_default(attribute)
|
|
597
|
+
value = attribute.default_value.to_s
|
|
598
|
+
case attribute.type
|
|
599
|
+
when :boolean then value.casecmp?('true')
|
|
600
|
+
when :integer, :long, :autonumber then value.empty? ? nil : Integer(value)
|
|
601
|
+
when :decimal then value.empty? ? nil : Float(value)
|
|
602
|
+
when :string then value
|
|
603
|
+
else value.empty? ? nil : value
|
|
604
|
+
end
|
|
182
605
|
end
|
|
183
606
|
|
|
184
|
-
|
|
607
|
+
def register_model_lifecycle
|
|
608
|
+
return unless store.respond_to?(:on)
|
|
609
|
+
|
|
610
|
+
@project.modules.each do |mod|
|
|
611
|
+
mod.entities.each do |entity|
|
|
612
|
+
qualified = "#{mod.name}.#{entity.name}"
|
|
613
|
+
Array(entity.respond_to?(:lifecycle) ? entity.lifecycle : nil).each do |callback|
|
|
614
|
+
event = callback.fetch(:event).to_sym
|
|
615
|
+
next unless Store::LIFECYCLE_EVENTS.include?(event)
|
|
616
|
+
|
|
617
|
+
handler = qualify_flow(mod.name, callback.fetch(:handler))
|
|
618
|
+
store.on(qualified, event) do |object|
|
|
619
|
+
result = call(handler, lifecycle_arguments(handler, object, callback))
|
|
620
|
+
if callback[:raise_error_on_false] && result == false
|
|
621
|
+
raise NativeRuntimeError, "entity lifecycle #{handler} rejected #{qualified}"
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
def qualify_flow(module_name, name)
|
|
630
|
+
value = name.to_s
|
|
631
|
+
value.include?('.') ? value : "#{module_name}.#{value}"
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def lifecycle_arguments(handler, object, callback)
|
|
635
|
+
return {} unless callback.fetch(:pass_event_object, true)
|
|
636
|
+
|
|
637
|
+
flow = @flows[handler]
|
|
638
|
+
parameter = flow&.parameters&.find { _1.is_a?(Hash) }
|
|
639
|
+
name = parameter && parameter['Name'].to_s
|
|
640
|
+
name.to_s.empty? ? {} : { name => object }
|
|
641
|
+
end
|
|
185
642
|
|
|
186
643
|
def execute(flow, variables)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
644
|
+
outgoing = flow.flows.group_by { identifier(_1['OriginPointer']) }
|
|
645
|
+
result = execute_collection(flow.objects, outgoing, variables, label: "microflow #{flow.name}", root: true)
|
|
646
|
+
result&.last
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def execute_collection(collection, outgoing, variables, label:, root: false)
|
|
650
|
+
plan = collection_plan(collection, outgoing, root:)
|
|
651
|
+
objects = plan.fetch(:objects)
|
|
652
|
+
current = plan.fetch(:entry)
|
|
653
|
+
raise NativeRuntimeError, "#{label} has no start event" unless current
|
|
192
654
|
|
|
193
655
|
10_000.times do
|
|
194
656
|
type = current['$Type']
|
|
195
|
-
return @expression.evaluate(current['ReturnValue'], variables) if type == 'Microflows$EndEvent'
|
|
657
|
+
return [:return, @expression.evaluate(current['ReturnValue'], variables)] if type == 'Microflows$EndEvent'
|
|
658
|
+
return [:break, nil] if type == 'Microflows$BreakEvent'
|
|
659
|
+
return [:continue, nil] if type == 'Microflows$ContinueEvent'
|
|
660
|
+
|
|
661
|
+
if type == 'Microflows$ErrorEvent'
|
|
662
|
+
raise NativeRuntimeError, @expression.evaluate(current['ErrorExpression'], variables).to_s
|
|
663
|
+
end
|
|
196
664
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
665
|
+
edges = outgoing[identifier(current)] || []
|
|
666
|
+
error_edge = edges.find { _1['IsErrorHandler'] == true }
|
|
667
|
+
action_snapshot = nil
|
|
668
|
+
begin
|
|
669
|
+
if type == 'Microflows$ActionActivity' && error_edge &&
|
|
670
|
+
current.dig('Action', 'ErrorHandlingType') == 'Rollback'
|
|
671
|
+
action_snapshot = store.snapshot
|
|
672
|
+
end
|
|
673
|
+
execute_action(current['Action'], variables) if type == 'Microflows$ActionActivity'
|
|
674
|
+
execute_loop(current, outgoing, variables, label:) if type == 'Microflows$LoopedActivity'
|
|
675
|
+
edge = select_edge(current, edges.reject { _1['IsErrorHandler'] == true }, variables)
|
|
676
|
+
rescue StandardError => e
|
|
677
|
+
edge = error_edge
|
|
678
|
+
raise unless edge
|
|
679
|
+
|
|
680
|
+
if action_snapshot && current.dig('Action', 'ErrorHandlingType') == 'Rollback'
|
|
681
|
+
store.restore(action_snapshot)
|
|
682
|
+
end
|
|
683
|
+
variables['latestError'] = e
|
|
684
|
+
end
|
|
685
|
+
return [:complete, nil] unless edge || root
|
|
686
|
+
raise NativeRuntimeError, "#{label} stops at #{type}" unless edge
|
|
200
687
|
|
|
201
|
-
|
|
688
|
+
destination = identifier(edge['DestinationPointer'])
|
|
689
|
+
current = objects[destination]
|
|
690
|
+
return [:complete, nil] unless current || root
|
|
202
691
|
raise NativeRuntimeError, 'sequence flow points to a missing object' unless current
|
|
203
692
|
end
|
|
204
|
-
raise NativeRuntimeError, "
|
|
693
|
+
raise NativeRuntimeError, "#{label} exceeded 10000 steps"
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def collection_plan(collection, outgoing, root:)
|
|
697
|
+
plans = @collection_plans.fetch(root)
|
|
698
|
+
plans[collection] ||= begin
|
|
699
|
+
list = items(collection).select { _1.is_a?(Hash) }
|
|
700
|
+
objects = list.to_h { [identifier(_1), _1] }
|
|
701
|
+
{ objects:, entry: collection_entry(list, objects, outgoing, root:) }.freeze
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def collection_entry(list, objects, outgoing, root:)
|
|
706
|
+
start = list.find { _1['$Type'] == 'Microflows$StartEvent' }
|
|
707
|
+
return start if start || root
|
|
708
|
+
|
|
709
|
+
destinations = objects.keys.to_h { [_1, false] }
|
|
710
|
+
objects.each_key do |origin|
|
|
711
|
+
Array(outgoing[origin]).each do |edge|
|
|
712
|
+
destination = identifier(edge['DestinationPointer'])
|
|
713
|
+
destinations[destination] = true if destinations.key?(destination)
|
|
714
|
+
end
|
|
715
|
+
end
|
|
716
|
+
list.find do |object|
|
|
717
|
+
!destinations[identifier(object)] && executable_object?(object)
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def executable_object?(object)
|
|
722
|
+
!%w[Microflows$Annotation Microflows$MicroflowParameter].include?(object['$Type'])
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
def execute_loop(activity, outgoing, variables, label:)
|
|
726
|
+
source = activity['LoopSource'] || {}
|
|
727
|
+
nested = activity.dig('ObjectCollection', 'Objects')
|
|
728
|
+
case source['$Type']
|
|
729
|
+
when 'Microflows$WhileLoopCondition'
|
|
730
|
+
execute_while_loop(source, nested, outgoing, variables, label:)
|
|
731
|
+
when 'Microflows$IterableList'
|
|
732
|
+
execute_iterable_loop(source, nested, outgoing, variables, label:)
|
|
733
|
+
else
|
|
734
|
+
raise NativeRuntimeError, "unsupported loop source #{source['$Type']}"
|
|
735
|
+
end
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
def execute_while_loop(source, nested, outgoing, variables, label:)
|
|
739
|
+
10_000.times do
|
|
740
|
+
return unless @expression.evaluate(source['WhileExpression'], variables)
|
|
741
|
+
|
|
742
|
+
result = execute_collection(nested, outgoing, variables, label: "#{label} loop")
|
|
743
|
+
return if result&.first == :break
|
|
744
|
+
end
|
|
745
|
+
raise NativeRuntimeError, "#{label} loop exceeded 10000 iterations"
|
|
746
|
+
end
|
|
747
|
+
|
|
748
|
+
def execute_iterable_loop(source, nested, outgoing, variables, label:)
|
|
749
|
+
values = Array(variables.fetch(source['ListVariableName'].to_s))
|
|
750
|
+
values.each do |value|
|
|
751
|
+
variables[source['VariableName'].to_s] = value
|
|
752
|
+
result = execute_collection(nested, outgoing, variables, label: "#{label} loop")
|
|
753
|
+
break if result&.first == :break
|
|
754
|
+
end
|
|
205
755
|
end
|
|
206
756
|
|
|
207
757
|
def select_edge(object, edges, variables)
|
|
@@ -221,9 +771,11 @@ module Mxrb
|
|
|
221
771
|
end
|
|
222
772
|
|
|
223
773
|
def flow_case(edge)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
774
|
+
@flow_case_cache[edge] ||= begin
|
|
775
|
+
values = items(edge['CaseValues'])
|
|
776
|
+
value = values.first || edge['NewCaseValue'] || {}
|
|
777
|
+
value['$Type'] == 'Microflows$NoCase' ? '' : value['Value'].to_s
|
|
778
|
+
end
|
|
227
779
|
end
|
|
228
780
|
|
|
229
781
|
def execute_action(action, variables)
|
|
@@ -237,15 +789,22 @@ module Mxrb
|
|
|
237
789
|
end
|
|
238
790
|
|
|
239
791
|
def action_create_change(action, variables)
|
|
240
|
-
|
|
792
|
+
entity = action['Entity'].to_s
|
|
793
|
+
authorize_entity!(entity, :create)
|
|
794
|
+
object = store.create(entity, events: action_events(action))
|
|
241
795
|
apply_changes(object, action['Items'], variables)
|
|
242
796
|
variables[action['VariableName'].to_s] = object
|
|
797
|
+
commit_for_action(action, object)
|
|
243
798
|
end
|
|
244
799
|
|
|
245
800
|
def action_change(action, variables)
|
|
246
801
|
object = variables.fetch(action['ChangeVariableName'].to_s)
|
|
802
|
+
authorize_entity!(object.entity, :write, record: object)
|
|
247
803
|
apply_changes(object, action['Items'], variables)
|
|
804
|
+
commit_for_action(action, object)
|
|
248
805
|
end
|
|
806
|
+
alias action_create_object action_create_change
|
|
807
|
+
alias action_change_object action_change
|
|
249
808
|
|
|
250
809
|
def action_create_variable(action, variables)
|
|
251
810
|
variables[action['VariableName'].to_s] = @expression.evaluate(action['InitialValue'], variables)
|
|
@@ -257,11 +816,16 @@ module Mxrb
|
|
|
257
816
|
|
|
258
817
|
def action_retrieve(action, variables)
|
|
259
818
|
source = action['RetrieveSource'] || {}
|
|
819
|
+
if source['$Type'] == 'Microflows$AssociationRetrieveSource'
|
|
820
|
+
return action_association_retrieve(action, source, variables)
|
|
821
|
+
end
|
|
260
822
|
unless source['$Type'] == 'Microflows$DatabaseRetrieveSource'
|
|
261
823
|
raise NativeRuntimeError, "unsupported retrieve source #{source['$Type']}"
|
|
262
824
|
end
|
|
263
825
|
|
|
264
|
-
|
|
826
|
+
entity = source['Entity'].to_s
|
|
827
|
+
values = filter_by_xpath(store.retrieve(entity), source['XpathConstraint'].to_s, variables)
|
|
828
|
+
values = @policy.filter_readable(entity, values, context: @security_context) if enforce_entity_access?
|
|
265
829
|
values = sort_values(values, items(source.dig('NewSortings', 'Sortings')))
|
|
266
830
|
range = source['Range'] || {}
|
|
267
831
|
limit = @expression.evaluate(range['LimitExpression'], variables)
|
|
@@ -269,6 +833,25 @@ module Mxrb
|
|
|
269
833
|
variables[action['ResultVariableName'].to_s] = range['SingleObject'] == true ? values.first : values
|
|
270
834
|
end
|
|
271
835
|
|
|
836
|
+
def action_association_retrieve(action, source, variables)
|
|
837
|
+
variable = source['StartVariableName'].to_s
|
|
838
|
+
association = source['AssociationId'].to_s.split('.').last
|
|
839
|
+
if variable.empty? || association.empty?
|
|
840
|
+
raise NativeRuntimeError, "unsupported retrieve source #{source['$Type']}"
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
start = variables.fetch(variable)
|
|
844
|
+
values = store.retrieve_association(association, start)
|
|
845
|
+
if enforce_entity_access?
|
|
846
|
+
values = Array(values).select do |value|
|
|
847
|
+
@policy.entity_allowed?(value.entity, action: :read, context: @security_context, record: value)
|
|
848
|
+
end
|
|
849
|
+
end
|
|
850
|
+
definition = @associations[source['AssociationId'].to_s]
|
|
851
|
+
values = values.first if definition&.fetch(:type) == :Reference && definition[:from] == start.entity
|
|
852
|
+
variables[action['ResultVariableName'].to_s] = values
|
|
853
|
+
end
|
|
854
|
+
|
|
272
855
|
# Narrows a value set by a Mendix XPath constraint. Only attribute
|
|
273
856
|
# predicates (comparisons, and/or, boolean shorthand) are understood;
|
|
274
857
|
# anything else is rejected by the expression evaluator rather than
|
|
@@ -344,10 +927,50 @@ module Mxrb
|
|
|
344
927
|
variables[action['ResultVariableName'].to_s] = result if action['UseReturnVariable'] == true
|
|
345
928
|
end
|
|
346
929
|
|
|
347
|
-
def action_commit(
|
|
930
|
+
def action_commit(action, variables)
|
|
931
|
+
variable = action['CommitVariableName'].to_s
|
|
932
|
+
return if variable.empty?
|
|
933
|
+
|
|
934
|
+
value = variables.fetch(variable)
|
|
935
|
+
Array(value).each { authorize_entity!(_1.entity, :write, record: _1) } if enforce_entity_access?
|
|
936
|
+
store.commit(value, events: action.fetch('WithEvents', true) != false) if store.respond_to?(:commit)
|
|
937
|
+
end
|
|
938
|
+
alias action_commit_objects action_commit
|
|
939
|
+
|
|
940
|
+
def commit_for_action(action, value)
|
|
941
|
+
setting = action['Commit'].is_a?(Hash) ? action['Commit']['Value'] : action['Commit']
|
|
942
|
+
return if setting.to_s.empty? || setting.to_s.casecmp?('No')
|
|
943
|
+
|
|
944
|
+
store.commit(value, events: !setting.to_s.match?(/WithoutEvents/i))
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
def action_events(action)
|
|
948
|
+
setting = action['Commit'].is_a?(Hash) ? action['Commit']['Value'] : action['Commit']
|
|
949
|
+
!setting.to_s.match?(/WithoutEvents/i)
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
def action_create_list(action, variables)
|
|
953
|
+
variables[action['VariableName'].to_s] = []
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def action_change_list(action, variables)
|
|
957
|
+
list = variables.fetch(action['ChangeVariableName'].to_s)
|
|
958
|
+
value = @expression.evaluate(action['Value'], variables)
|
|
959
|
+
case action['Type'].to_s.downcase
|
|
960
|
+
when 'add' then list << value unless list.include?(value)
|
|
961
|
+
when 'remove' then list.delete(value)
|
|
962
|
+
when 'clear' then list.clear
|
|
963
|
+
else raise NativeRuntimeError, "unsupported list change #{action['Type']}"
|
|
964
|
+
end
|
|
965
|
+
end
|
|
348
966
|
|
|
349
967
|
def action_delete(action, variables)
|
|
350
|
-
|
|
968
|
+
value = variables.fetch(action['DeleteVariableName'].to_s)
|
|
969
|
+
Array(value).each { authorize_entity!(_1.entity, :delete, record: _1) } if enforce_entity_access?
|
|
970
|
+
store.delete(
|
|
971
|
+
value,
|
|
972
|
+
events: action.fetch('WithEvents', true) != false
|
|
973
|
+
)
|
|
351
974
|
end
|
|
352
975
|
|
|
353
976
|
def action_log_message(action, variables)
|
|
@@ -356,6 +979,274 @@ module Mxrb
|
|
|
356
979
|
|
|
357
980
|
def action_cast(_action, _variables); end
|
|
358
981
|
|
|
982
|
+
def action_rollback(action, variables)
|
|
983
|
+
value = variables.fetch(action['RollbackVariableName'].to_s)
|
|
984
|
+
store.rollback(value) if store.respond_to?(:rollback)
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
def action_show_message(action, variables)
|
|
988
|
+
template = action['Template'] || {}
|
|
989
|
+
@effects << {
|
|
990
|
+
type: 'show_message', level: action['Type'].to_s.downcase,
|
|
991
|
+
blocking: action['Blocking'] == true,
|
|
992
|
+
message: render_text_template(template, variables)
|
|
993
|
+
}
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
def action_validation_feedback(action, variables)
|
|
997
|
+
object = variables.fetch(action['ValidationVariableName'].to_s)
|
|
998
|
+
member = (action['Attribute'].to_s.empty? ? action['Association'] : action['Attribute']).to_s
|
|
999
|
+
@effects << {
|
|
1000
|
+
type: 'validation_feedback', object_id: object.id,
|
|
1001
|
+
member: member.split(%r{[./]}).last,
|
|
1002
|
+
message: render_text_template(action['FeedbackTemplate'] || {}, variables)
|
|
1003
|
+
}
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
def action_list_operations(action, variables)
|
|
1007
|
+
operation = action['NewOperation'] || {}
|
|
1008
|
+
list = Array(variables.fetch(operation['ListName'].to_s))
|
|
1009
|
+
second = variables[operation['SecondListOrObjectName'].to_s]
|
|
1010
|
+
result = list_operation(operation['$Type'], list, second, operation['Expression'], variables)
|
|
1011
|
+
variables[action['ResultVariableName'].to_s] = result
|
|
1012
|
+
end
|
|
1013
|
+
|
|
1014
|
+
def action_java_action_call(action, variables)
|
|
1015
|
+
name = action['JavaAction'].to_s
|
|
1016
|
+
adapter = @java_custom_actions[name]
|
|
1017
|
+
unless adapter
|
|
1018
|
+
raise NativeRuntimeError,
|
|
1019
|
+
"Java Custom Action #{name.empty? ? '(missing name)' : name} is not registered; " \
|
|
1020
|
+
'register an explicit Ruby adapter with ' \
|
|
1021
|
+
'Mxrb::RubyApp::Registry.register_java_custom_action'
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
result = adapter.call(java_action_arguments(name, action, variables).freeze)
|
|
1025
|
+
result_name = (action['ResultVariableName'] || action['OutputVariableName']).to_s
|
|
1026
|
+
use_return = action.key?('UseReturnVariable') ? action['UseReturnVariable'] == true : !result_name.empty?
|
|
1027
|
+
variables[result_name] = result if use_return && !result_name.empty?
|
|
1028
|
+
result
|
|
1029
|
+
end
|
|
1030
|
+
alias action_java_action action_java_action_call
|
|
1031
|
+
alias action_basic_java action_java_action_call
|
|
1032
|
+
alias action_basic_code action_java_action_call
|
|
1033
|
+
alias action_entity_type_java action_java_action_call
|
|
1034
|
+
alias action_microflow_java action_java_action_call
|
|
1035
|
+
|
|
1036
|
+
def java_action_arguments(name, action, variables)
|
|
1037
|
+
parameter_names = java_action_parameter_names(name)
|
|
1038
|
+
items(action['ParameterMappings']).to_h do |mapping|
|
|
1039
|
+
reference = identifier(mapping['Parameter'])
|
|
1040
|
+
parameter_name = parameter_names.fetch(reference, reference.to_s.split('.').last)
|
|
1041
|
+
if parameter_name.to_s.empty?
|
|
1042
|
+
raise NativeRuntimeError, "Java Custom Action #{name} has an unnamed parameter mapping"
|
|
1043
|
+
end
|
|
1044
|
+
|
|
1045
|
+
[parameter_name, java_action_argument(name, parameter_name, mapping['Value'], variables)]
|
|
1046
|
+
end
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def java_action_argument(action_name, parameter_name, value, variables)
|
|
1050
|
+
value ||= {}
|
|
1051
|
+
type = value['$Type'].to_s.delete_prefix('Microflows$')
|
|
1052
|
+
case type
|
|
1053
|
+
when 'BasicJavaActionParameterValue', 'BasicCodeActionParameterValue'
|
|
1054
|
+
@expression.evaluate(value['Argument'], variables)
|
|
1055
|
+
when 'EntityTypeJavaActionParameterValue'
|
|
1056
|
+
value['Entity'].to_s
|
|
1057
|
+
when 'MicroflowJavaActionParameterValue'
|
|
1058
|
+
value['Microflow'].to_s
|
|
1059
|
+
when 'ImportMappingJavaActionParameterValue'
|
|
1060
|
+
value['ImportMapping'].to_s
|
|
1061
|
+
when 'ExportMappingJavaActionParameterValue'
|
|
1062
|
+
value['ExportMapping'].to_s
|
|
1063
|
+
else
|
|
1064
|
+
raise NativeRuntimeError,
|
|
1065
|
+
"Java Custom Action #{action_name} parameter #{parameter_name} " \
|
|
1066
|
+
"uses unsupported mapping #{value['$Type'].inspect}"
|
|
1067
|
+
end
|
|
1068
|
+
end
|
|
1069
|
+
|
|
1070
|
+
def java_action_parameter_names(name)
|
|
1071
|
+
@java_action_parameter_names[name] ||= begin
|
|
1072
|
+
artifact = @project.find_artifact(name, kind: :java_action)
|
|
1073
|
+
document = artifact && @project.parse_bson(@project.raw_unit(artifact.unit_id))
|
|
1074
|
+
items(document&.[]('Parameters')).to_h do |parameter|
|
|
1075
|
+
[identifier(parameter['$ID']), parameter['Name'].to_s]
|
|
1076
|
+
end
|
|
1077
|
+
end
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
def action_java_script_action_call(action, variables)
|
|
1081
|
+
client_action(:javascript, action['JavaScriptAction'], action, variables,
|
|
1082
|
+
result_name: action['OutputVariableName'])
|
|
1083
|
+
end
|
|
1084
|
+
|
|
1085
|
+
def action_nanoflow_call(action, variables)
|
|
1086
|
+
call = action['NanoflowCall'] || {}
|
|
1087
|
+
client_action(:nanoflow, call['Nanoflow'], call, variables,
|
|
1088
|
+
result_name: action['OutputVariableName'])
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
def action_app_service_call(action, variables)
|
|
1092
|
+
invoke_adapter(:app_service, action['AppServiceAction'], action, variables,
|
|
1093
|
+
result_name: action['ResultVariableName'])
|
|
1094
|
+
end
|
|
1095
|
+
|
|
1096
|
+
def action_call_web_service(action, variables)
|
|
1097
|
+
invoke_adapter(
|
|
1098
|
+
:web_service, action['WebServiceCall'] || action['Operation'], action, variables,
|
|
1099
|
+
result_name: action['ResultVariableName']
|
|
1100
|
+
)
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
def action_import_xml(action, variables)
|
|
1104
|
+
invoke_adapter(
|
|
1105
|
+
:import_xml, action['ImportMapping'], action, variables,
|
|
1106
|
+
result_name: action['ResultVariableName']
|
|
1107
|
+
)
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
def action_generate_document(action, variables)
|
|
1111
|
+
invoke_adapter(
|
|
1112
|
+
:document, action['DocumentTemplate'], action, variables,
|
|
1113
|
+
result_name: action['ResultVariableName']
|
|
1114
|
+
)
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1117
|
+
def action_download_file(action, variables)
|
|
1118
|
+
@effects << {
|
|
1119
|
+
type: :download_file,
|
|
1120
|
+
value: variables[action['FileDocumentVariableName'].to_s],
|
|
1121
|
+
show_in_browser: action['ShowFileInBrowser'] == true
|
|
1122
|
+
}
|
|
1123
|
+
end
|
|
1124
|
+
|
|
1125
|
+
def action_show_home_page(_action, _variables)
|
|
1126
|
+
@effects << { type: :show_home_page }
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def action_empty(_action, _variables); end
|
|
1130
|
+
|
|
1131
|
+
def action_import_mapping_java(action, variables)
|
|
1132
|
+
invoke_adapter(
|
|
1133
|
+
:import_mapping, action['ImportMapping'], action, variables,
|
|
1134
|
+
result_name: action['ResultVariableName']
|
|
1135
|
+
)
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
def action_export_mapping_java(action, variables)
|
|
1139
|
+
invoke_adapter(
|
|
1140
|
+
:export_mapping, action['ExportMapping'], action, variables,
|
|
1141
|
+
result_name: action['ResultVariableName']
|
|
1142
|
+
)
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
def action_rest_call(action, variables)
|
|
1146
|
+
configuration = action['HttpConfiguration'] || {}
|
|
1147
|
+
location = render_template(configuration['CustomLocationTemplate'], variables)
|
|
1148
|
+
headers = items(configuration['HttpHeaderEntries']).to_h do |entry|
|
|
1149
|
+
[entry['Key'].to_s, @expression.evaluate(entry['Value'], variables).to_s]
|
|
1150
|
+
rescue NativeRuntimeError
|
|
1151
|
+
[entry['Key'].to_s, entry['Value'].to_s]
|
|
1152
|
+
end
|
|
1153
|
+
request = action['RequestHandling'] || {}
|
|
1154
|
+
body_value = variables[request['MappingVariableName'].to_s]
|
|
1155
|
+
body = body_value.nil? ? nil : JSON.generate(runtime_json(body_value))
|
|
1156
|
+
timeout = @expression.evaluate(action['TimeOutExpression'], variables) if action['UseRequestTimeOut'] == true
|
|
1157
|
+
response = @http.call(configuration['HttpMethod'].to_s, location, headers, body, timeout)
|
|
1158
|
+
unless response.code.to_i.between?(200, 299)
|
|
1159
|
+
raise NativeRuntimeError, "REST call returned HTTP #{response.code}"
|
|
1160
|
+
end
|
|
1161
|
+
|
|
1162
|
+
bind_rest_result(action['ResultHandling'] || {}, response.body.to_s, variables)
|
|
1163
|
+
rescue URI::InvalidURIError, JSON::ParserError => e
|
|
1164
|
+
raise NativeRuntimeError, "REST call failed: #{e.message}"
|
|
1165
|
+
end
|
|
1166
|
+
|
|
1167
|
+
def action_show_form(action, variables)
|
|
1168
|
+
settings = action['FormSettings'] || {}
|
|
1169
|
+
arguments = items(settings['ParameterMappings']).to_h do |mapping|
|
|
1170
|
+
[mapping['Parameter'].to_s, @expression.evaluate(mapping['Argument'], variables)]
|
|
1171
|
+
end
|
|
1172
|
+
@effects << { type: 'open_page', page: settings['Form'].to_s, arguments: }
|
|
1173
|
+
end
|
|
1174
|
+
|
|
1175
|
+
def action_close_form(_action, _variables)
|
|
1176
|
+
@effects << { type: 'close_page' }
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
def list_operation(type, list, second, expression, variables)
|
|
1180
|
+
operation = type.to_s.delete_prefix('Microflows$').downcase
|
|
1181
|
+
case operation
|
|
1182
|
+
when 'head' then list.first
|
|
1183
|
+
when 'tail' then list.drop(1)
|
|
1184
|
+
when 'find' then list.find { @expression.evaluate(expression, variables, node: _1) }
|
|
1185
|
+
when 'filter' then list.select { @expression.evaluate(expression, variables, node: _1) }
|
|
1186
|
+
when 'sort' then sort_values(list, items(second))
|
|
1187
|
+
when 'union' then (list + Array(second)).uniq
|
|
1188
|
+
when 'intersect' then list & Array(second)
|
|
1189
|
+
when 'subtract' then list - Array(second)
|
|
1190
|
+
when 'contains' then list.include?(second)
|
|
1191
|
+
else raise NativeRuntimeError, "unsupported list operation #{type}"
|
|
1192
|
+
end
|
|
1193
|
+
end
|
|
1194
|
+
|
|
1195
|
+
def client_action(kind, name, document, variables, result_name:)
|
|
1196
|
+
return invoke_adapter(kind, name, document, variables, result_name:) if @adapters[kind]
|
|
1197
|
+
|
|
1198
|
+
@effects << { type: kind.to_s, name: name.to_s }
|
|
1199
|
+
variables[result_name.to_s] = nil unless result_name.to_s.empty?
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
def invoke_adapter(kind, name, document, variables, result_name:)
|
|
1203
|
+
adapter = @adapters[kind] or raise NativeRuntimeError, "#{kind} adapter is not configured"
|
|
1204
|
+
result = adapter.call(name.to_s, document, variables)
|
|
1205
|
+
variables[result_name.to_s] = result unless result_name.to_s.empty?
|
|
1206
|
+
result
|
|
1207
|
+
end
|
|
1208
|
+
|
|
1209
|
+
def bind_rest_result(handling, body, variables)
|
|
1210
|
+
return unless handling['Bind'] == true
|
|
1211
|
+
|
|
1212
|
+
result = JSON.parse(body)
|
|
1213
|
+
entity = handling.dig('VariableType', 'Entity').to_s
|
|
1214
|
+
result = rest_object(entity, result) unless entity.empty?
|
|
1215
|
+
variables[handling['ResultVariableName'].to_s] = result
|
|
1216
|
+
end
|
|
1217
|
+
|
|
1218
|
+
def rest_object(entity, value)
|
|
1219
|
+
authorize_entity!(entity, :create)
|
|
1220
|
+
object = store.create(entity)
|
|
1221
|
+
value.to_h.each do |key, child|
|
|
1222
|
+
authorize_entity!(entity, :write, member: key, record: object)
|
|
1223
|
+
object.members[key.to_s] = child
|
|
1224
|
+
end
|
|
1225
|
+
object
|
|
1226
|
+
end
|
|
1227
|
+
|
|
1228
|
+
def runtime_json(value)
|
|
1229
|
+
case value
|
|
1230
|
+
when ObjectValue then value.members.transform_values { runtime_json(_1) }
|
|
1231
|
+
when Hash then value.transform_values { runtime_json(_1) }
|
|
1232
|
+
when Array then value.map { runtime_json(_1) }
|
|
1233
|
+
else value
|
|
1234
|
+
end
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
def http_request(method, location, headers, body, timeout)
|
|
1238
|
+
uri = URI.parse(location)
|
|
1239
|
+
request_class = Net::HTTP.const_get(method.to_s.downcase.capitalize)
|
|
1240
|
+
request = request_class.new(uri)
|
|
1241
|
+
headers.each { request[_1] = _2 }
|
|
1242
|
+
request.body = body if body
|
|
1243
|
+
Net::HTTP.start(
|
|
1244
|
+
uri.host, uri.port, use_ssl: uri.scheme == 'https',
|
|
1245
|
+
open_timeout: timeout || 10,
|
|
1246
|
+
read_timeout: timeout || 30
|
|
1247
|
+
) { _1.request(request) }
|
|
1248
|
+
end
|
|
1249
|
+
|
|
359
1250
|
def aggregate(function, values)
|
|
360
1251
|
case function.to_s.downcase
|
|
361
1252
|
when 'count' then values.size
|
|
@@ -371,10 +1262,21 @@ module Mxrb
|
|
|
371
1262
|
items(values).each do |item|
|
|
372
1263
|
member = (item['Attribute'].to_s.empty? ? item['Association'] : item['Attribute']).to_s
|
|
373
1264
|
name = member.split(%r{[./]}).last
|
|
1265
|
+
authorize_entity!(object.entity, :write, member: name, record: object)
|
|
374
1266
|
object.members[name] = @expression.evaluate(item['Value'], variables)
|
|
375
1267
|
end
|
|
376
1268
|
end
|
|
377
1269
|
|
|
1270
|
+
def enforce_entity_access? = @apply_entity_access && @policy && @security_context
|
|
1271
|
+
|
|
1272
|
+
def authorize_entity!(entity, action, member: nil, record: nil)
|
|
1273
|
+
return true unless enforce_entity_access?
|
|
1274
|
+
|
|
1275
|
+
@policy.authorize!(
|
|
1276
|
+
entity, kind: :entity, action:, context: @security_context, member:, record:
|
|
1277
|
+
)
|
|
1278
|
+
end
|
|
1279
|
+
|
|
378
1280
|
def render_template(template, variables)
|
|
379
1281
|
text = template&.dig('Text').to_s
|
|
380
1282
|
items(template&.dig('Parameters')).each_with_index do |parameter, index|
|
|
@@ -383,6 +1285,21 @@ module Mxrb
|
|
|
383
1285
|
text
|
|
384
1286
|
end
|
|
385
1287
|
|
|
1288
|
+
def render_text_template(template, variables)
|
|
1289
|
+
text_doc = template&.dig('Text')
|
|
1290
|
+
text = if text_doc.is_a?(Hash)
|
|
1291
|
+
translation = items(text_doc['Items']).find { _1['LanguageCode'] == 'en_US' } ||
|
|
1292
|
+
items(text_doc['Items']).first
|
|
1293
|
+
translation&.fetch('Text', '').to_s
|
|
1294
|
+
else
|
|
1295
|
+
text_doc.to_s
|
|
1296
|
+
end
|
|
1297
|
+
items(template&.dig('Parameters')).each_with_index do |parameter, index|
|
|
1298
|
+
text = text.gsub("{#{index + 1}}", @expression.evaluate(parameter['Expression'], variables).to_s)
|
|
1299
|
+
end
|
|
1300
|
+
text
|
|
1301
|
+
end
|
|
1302
|
+
|
|
386
1303
|
def normalize_arguments(flow, arguments)
|
|
387
1304
|
names = flow.parameters.filter_map { _1['Name'] if _1.is_a?(Hash) }
|
|
388
1305
|
names.to_h do |name|
|
|
@@ -397,7 +1314,11 @@ module Mxrb
|
|
|
397
1314
|
end
|
|
398
1315
|
|
|
399
1316
|
def identifier(value)
|
|
400
|
-
|
|
1317
|
+
return value if value.is_a?(String)
|
|
1318
|
+
|
|
1319
|
+
@identifier_cache[value] ||= IO::BsonCodec.extract_id(
|
|
1320
|
+
value.is_a?(Hash) ? value['$ID'] : value
|
|
1321
|
+
).to_s
|
|
401
1322
|
end
|
|
402
1323
|
|
|
403
1324
|
def underscore(value)
|
|
@@ -467,8 +1388,9 @@ module Mxrb
|
|
|
467
1388
|
@clock.call
|
|
468
1389
|
end
|
|
469
1390
|
end
|
|
470
|
-
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
1391
|
+
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength, Metrics/CyclomaticComplexity
|
|
471
1392
|
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
1393
|
+
# rubocop:enable Metrics/ParameterLists
|
|
472
1394
|
end
|
|
473
1395
|
end
|
|
474
1396
|
end
|