mxrb 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.de-DE.md +108 -0
- data/README.md +172 -0
- data/README.pt-BR.md +109 -0
- data/bin/mxrb +1613 -0
- data/docs/README.md +10 -0
- data/docs/de-DE/README.md +24 -0
- data/docs/de-DE/architectural-patterns.md +26 -0
- data/docs/de-DE/architectural-standard.md +48 -0
- data/docs/de-DE/architecture.md +76 -0
- data/docs/de-DE/compiler.md +162 -0
- data/docs/de-DE/conventions.md +19 -0
- data/docs/de-DE/design-system.md +52 -0
- data/docs/de-DE/entity-dsl.md +53 -0
- data/docs/de-DE/native-runtime-quality-report.md +74 -0
- data/docs/de-DE/oql-sql.md +211 -0
- data/docs/de-DE/platform-api-integration.md +36 -0
- data/docs/de-DE/platform-operations.md +243 -0
- data/docs/de-DE/project-structure.md +32 -0
- data/docs/de-DE/ruby-first-roadmap.md +116 -0
- data/docs/de-DE/scaffolds.md +59 -0
- data/docs/de-DE/semantic-refactoring.md +56 -0
- data/docs/de-DE/team-server.md +33 -0
- data/docs/de-DE/validation-matrix.md +64 -0
- data/docs/de-DE/vetclinic-acceptance.md +24 -0
- data/docs/de-DE/writing.md +138 -0
- data/docs/en-US/README.md +24 -0
- data/docs/en-US/architectural-patterns.md +151 -0
- data/docs/en-US/architectural-standard.md +89 -0
- data/docs/en-US/architecture.md +170 -0
- data/docs/en-US/compiler.md +157 -0
- data/docs/en-US/conventions.md +82 -0
- data/docs/en-US/design-system.md +163 -0
- data/docs/en-US/entity-dsl.md +53 -0
- data/docs/en-US/native-runtime-quality-report.md +73 -0
- data/docs/en-US/oql-sql.md +201 -0
- data/docs/en-US/platform-api-integration.md +36 -0
- data/docs/en-US/platform-operations.md +244 -0
- data/docs/en-US/project-structure.md +49 -0
- data/docs/en-US/ruby-first-roadmap.md +186 -0
- data/docs/en-US/scaffolds.md +57 -0
- data/docs/en-US/semantic-refactoring.md +198 -0
- data/docs/en-US/team-server.md +32 -0
- data/docs/en-US/validation-matrix.md +257 -0
- data/docs/en-US/vetclinic-acceptance.md +23 -0
- data/docs/en-US/writing.md +655 -0
- data/docs/pt-BR/README.md +24 -0
- data/docs/pt-BR/architectural-patterns.md +25 -0
- data/docs/pt-BR/architectural-standard.md +533 -0
- data/docs/pt-BR/architecture.md +300 -0
- data/docs/pt-BR/compiler.md +162 -0
- data/docs/pt-BR/conventions.md +19 -0
- data/docs/pt-BR/design-system.md +65 -0
- data/docs/pt-BR/entity-dsl.md +116 -0
- data/docs/pt-BR/native-runtime-quality-report.md +73 -0
- data/docs/pt-BR/oql-sql.md +208 -0
- data/docs/pt-BR/platform-api-integration.md +36 -0
- data/docs/pt-BR/platform-operations.md +260 -0
- data/docs/pt-BR/project-structure.md +32 -0
- data/docs/pt-BR/ruby-first-roadmap.md +239 -0
- data/docs/pt-BR/scaffolds.md +104 -0
- data/docs/pt-BR/semantic-refactoring.md +55 -0
- data/docs/pt-BR/team-server.md +59 -0
- data/docs/pt-BR/validation-matrix.md +86 -0
- data/docs/pt-BR/vetclinic-acceptance.md +50 -0
- data/docs/pt-BR/writing.md +177 -0
- data/examples/sudoku_evaluation.rb +14 -0
- data/examples/sudoku_functional_test.rb +28 -0
- data/lib/mxrb/architecture/graph.rb +124 -0
- data/lib/mxrb/architecture/validator.rb +98 -0
- data/lib/mxrb/benchmark.rb +33 -0
- data/lib/mxrb/compare.rb +390 -0
- data/lib/mxrb/compiler/adapter.rb +96 -0
- data/lib/mxrb/compiler/artifact_document_compiler.rb +102 -0
- data/lib/mxrb/compiler/artifact_materializer.rb +53 -0
- data/lib/mxrb/compiler/client_model_materializer.rb +42 -0
- data/lib/mxrb/compiler/code_action_document_compiler.rb +64 -0
- data/lib/mxrb/compiler/code_action_materializer.rb +35 -0
- data/lib/mxrb/compiler/code_action_type_compiler.rb +32 -0
- data/lib/mxrb/compiler/combo_box_bundle_compiler.rb +314 -0
- data/lib/mxrb/compiler/compatibility_analyzer.rb +169 -0
- data/lib/mxrb/compiler/constants_materializer.rb +111 -0
- data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +290 -0
- data/lib/mxrb/compiler/database_connector_action_compiler.rb +280 -0
- data/lib/mxrb/compiler/deployment_asset_copier.rb +60 -0
- data/lib/mxrb/compiler/deployment_bootstrapper.rb +208 -0
- data/lib/mxrb/compiler/deployment_materializer.rb +43 -0
- data/lib/mxrb/compiler/deployment_metadata.rb +64 -0
- data/lib/mxrb/compiler/domain_document_compiler.rb +153 -0
- data/lib/mxrb/compiler/domain_model_materializer.rb +35 -0
- data/lib/mxrb/compiler/domain_security_compiler.rb +82 -0
- data/lib/mxrb/compiler/gallery_bundle_compiler.rb +190 -0
- data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +281 -0
- data/lib/mxrb/compiler/image_bundle_compiler.rb +216 -0
- data/lib/mxrb/compiler/java_proxy_generator.rb +509 -0
- data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +267 -0
- data/lib/mxrb/compiler/legacy_page_builder.rb +287 -0
- data/lib/mxrb/compiler/mda.rb +99 -0
- data/lib/mxrb/compiler/microflow_document_compiler.rb +85 -0
- data/lib/mxrb/compiler/microflow_materializer.rb +48 -0
- data/lib/mxrb/compiler/microflow_node_compiler.rb +250 -0
- data/lib/mxrb/compiler/model_package.rb +116 -0
- data/lib/mxrb/compiler/model_values.rb +67 -0
- data/lib/mxrb/compiler/nanoflow_program_compiler.rb +483 -0
- data/lib/mxrb/compiler/navigation_document_compiler.rb +93 -0
- data/lib/mxrb/compiler/packager.rb +100 -0
- data/lib/mxrb/compiler/page_bundle_builder.rb +56 -0
- data/lib/mxrb/compiler/page_bundle_compiler.rb +2004 -0
- data/lib/mxrb/compiler/page_document_compiler.rb +77 -0
- data/lib/mxrb/compiler/portable_packager.rb +356 -0
- data/lib/mxrb/compiler/project_jar_archive.rb +84 -0
- data/lib/mxrb/compiler/project_jar_builder.rb +121 -0
- data/lib/mxrb/compiler/project_materializer.rb +139 -0
- data/lib/mxrb/compiler/project_model_orderer.rb +52 -0
- data/lib/mxrb/compiler/runtime_data_types.rb +31 -0
- data/lib/mxrb/compiler/runtime_model_schema.rb +90 -0
- data/lib/mxrb/compiler/schemas/runtime-10.24.0.73019.json +1052 -0
- data/lib/mxrb/compiler/schemas/runtime-11.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-6.10.8.json +1148 -0
- data/lib/mxrb/compiler/schemas/runtime-7.17.0.json +1144 -0
- data/lib/mxrb/compiler/schemas/runtime-7.5.0.json +1215 -0
- data/lib/mxrb/compiler/schemas/runtime-9.6.1.29396.json +868 -0
- data/lib/mxrb/compiler/schemas/system-model-10.24.0.73019.b64 +1135 -0
- data/lib/mxrb/compiler/schemas/system-model-11.12.1.b64 +815 -0
- data/lib/mxrb/compiler/schemas/system-model-6.10.8.b64 +497 -0
- data/lib/mxrb/compiler/schemas/system-model-7.17.0.b64 +519 -0
- data/lib/mxrb/compiler/schemas/system-model-7.5.0.b64 +504 -0
- data/lib/mxrb/compiler/schemas/system-model-9.6.1.29396.b64 +816 -0
- data/lib/mxrb/compiler/security_materializer.rb +104 -0
- data/lib/mxrb/compiler/settings_document_compiler.rb +45 -0
- data/lib/mxrb/compiler/settings_materializer.rb +25 -0
- data/lib/mxrb/compiler/source_model.rb +128 -0
- data/lib/mxrb/compiler/system_model_seed.rb +88 -0
- data/lib/mxrb/compiler/system_queue_materializer.rb +54 -0
- data/lib/mxrb/compiler/system_text_materializer.rb +49 -0
- data/lib/mxrb/compiler/translation_materializer.rb +95 -0
- data/lib/mxrb/compiler/web_bundle_builder.rb +155 -0
- data/lib/mxrb/compiler/web_list_data_source.rb +139 -0
- data/lib/mxrb/compiler/web_operation_compiler.rb +614 -0
- data/lib/mxrb/compiler/web_shell_materializer.rb +194 -0
- data/lib/mxrb/compiler/widget_package_extractor.rb +63 -0
- data/lib/mxrb/doctor.rb +116 -0
- data/lib/mxrb/dsl/builder.rb +1636 -0
- data/lib/mxrb/errors.rb +18 -0
- data/lib/mxrb/evaluation.rb +131 -0
- data/lib/mxrb/exporter.rb +1716 -0
- data/lib/mxrb/frontend/migrator.rb +775 -0
- data/lib/mxrb/functional.rb +307 -0
- data/lib/mxrb/github/annotator.rb +161 -0
- data/lib/mxrb/initializer.rb +257 -0
- data/lib/mxrb/integrity/validator.rb +136 -0
- data/lib/mxrb/io/bson_codec.rb +145 -0
- data/lib/mxrb/io/mpr_file.rb +558 -0
- data/lib/mxrb/io/mxunit_codec.rb +42 -0
- data/lib/mxrb/marketplace.rb +380 -0
- data/lib/mxrb/model/association.rb +98 -0
- data/lib/mxrb/model/attribute.rb +100 -0
- data/lib/mxrb/model/connector.rb +17 -0
- data/lib/mxrb/model/design_materializer.rb +133 -0
- data/lib/mxrb/model/design_migration.rb +126 -0
- data/lib/mxrb/model/design_system.rb +111 -0
- data/lib/mxrb/model/domain_model.rb +51 -0
- data/lib/mxrb/model/entity.rb +180 -0
- data/lib/mxrb/model/menu.rb +38 -0
- data/lib/mxrb/model/microflow.rb +100 -0
- data/lib/mxrb/model/module.rb +142 -0
- data/lib/mxrb/model/navigation.rb +127 -0
- data/lib/mxrb/model/page.rb +410 -0
- data/lib/mxrb/model/project.rb +187 -0
- data/lib/mxrb/model/unit.rb +48 -0
- data/lib/mxrb/module_initializer.rb +75 -0
- data/lib/mxrb/official_marketplace/content_api.rb +292 -0
- data/lib/mxrb/official_marketplace/dependency_resolver.rb +329 -0
- data/lib/mxrb/official_marketplace/lifecycle.rb +367 -0
- data/lib/mxrb/official_marketplace/module_package_importer.rb +318 -0
- data/lib/mxrb/official_marketplace/widget_package_installer.rb +414 -0
- data/lib/mxrb/official_marketplace.rb +720 -0
- data/lib/mxrb/oql/analyzer.rb +155 -0
- data/lib/mxrb/oql/index_advisor.rb +85 -0
- data/lib/mxrb/oql/plan_analyzer.rb +185 -0
- data/lib/mxrb/oql/server.rb +132 -0
- data/lib/mxrb/oql/sql_server_plan_analyzer.rb +190 -0
- data/lib/mxrb/oql/sql_server_workload_analyzer.rb +80 -0
- data/lib/mxrb/oql/workload_analyzer.rb +175 -0
- data/lib/mxrb/oql/workload_baseline.rb +60 -0
- data/lib/mxrb/oql.rb +424 -0
- data/lib/mxrb/project_lifecycle.rb +76 -0
- data/lib/mxrb/protocols/plan.rb +86 -0
- data/lib/mxrb/protocols.rb +123 -0
- data/lib/mxrb/runtime/database_workspace.rb +677 -0
- data/lib/mxrb/runtime/docker_executor.rb +41 -0
- data/lib/mxrb/runtime/docker_workspace.rb +29 -0
- data/lib/mxrb/runtime/executor.rb +169 -0
- data/lib/mxrb/runtime/java_locator.rb +26 -0
- data/lib/mxrb/runtime/native.rb +474 -0
- data/lib/mxrb/runtime/sql_server_database.rb +146 -0
- data/lib/mxrb/runtime/toolchain.rb +82 -0
- data/lib/mxrb/scaffold/cli.rb +113 -0
- data/lib/mxrb/scaffold/generator.rb +246 -0
- data/lib/mxrb/scaffold/help.rb +80 -0
- data/lib/mxrb/scaffold/page_templates.rb +54 -0
- data/lib/mxrb/scaffold/recipes.rb +198 -0
- data/lib/mxrb/scaffold/registry.rb +82 -0
- data/lib/mxrb/scaffold/templates.rb +723 -0
- data/lib/mxrb/scaffold/transaction.rb +84 -0
- data/lib/mxrb/schema/tables.rb +55 -0
- data/lib/mxrb/semantic/analyzer.rb +473 -0
- data/lib/mxrb/semantic/batch_plan.rb +68 -0
- data/lib/mxrb/semantic/domain_mutator.rb +419 -0
- data/lib/mxrb/semantic/embedder.rb +51 -0
- data/lib/mxrb/semantic/extractor.rb +290 -0
- data/lib/mxrb/semantic/index.rb +623 -0
- data/lib/mxrb/semantic/inliner.rb +216 -0
- data/lib/mxrb/semantic/mover.rb +153 -0
- data/lib/mxrb/semantic/onnx_embedder.rb +34 -0
- data/lib/mxrb/semantic/remover.rb +70 -0
- data/lib/mxrb/semantic/renamer.rb +211 -0
- data/lib/mxrb/semantic/tfidf_embedder.rb +46 -0
- data/lib/mxrb/semantic/vec_store.rb +75 -0
- data/lib/mxrb/team_server.rb +390 -0
- data/lib/mxrb/templates/project/10.24.0.73019.json +32 -0
- data/lib/mxrb/templates/project/11.12.1.json +32 -0
- data/lib/mxrb/templates/project/6.10.8.json +50 -0
- data/lib/mxrb/templates/project/7.17.0.json +50 -0
- data/lib/mxrb/templates/project/7.5.0.json +50 -0
- data/lib/mxrb/templates/project/9.6.1.29396.json +50 -0
- data/lib/mxrb/version.rb +5 -0
- data/lib/mxrb/widget_package.rb +338 -0
- data/lib/mxrb/widget_synchronizer.rb +31 -0
- data/lib/mxrb/writer.rb +3775 -0
- data/lib/mxrb.rb +188 -0
- data/marketplace/catalog.json +10 -0
- data/marketplace/modules/shared-kernel/domain/README.md +4 -0
- data/marketplace/modules/shared-kernel/module.rb +5 -0
- data/marketplace/modules/shared-kernel/mxrb-module.json +9 -0
- metadata +447 -0
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
module OfficialMarketplace
|
|
5
|
+
# Immutable preview whose mutation is guarded by explicit apply and blockers.
|
|
6
|
+
class LifecyclePlan
|
|
7
|
+
attr_reader :action, :name, :installed_version, :target_version, :changes, :blockers
|
|
8
|
+
|
|
9
|
+
# rubocop:disable Metrics/ParameterLists
|
|
10
|
+
def initialize(action:, name:, installed_version:, target_version:, changes:, blockers:, &operation)
|
|
11
|
+
@action = action
|
|
12
|
+
@name = name
|
|
13
|
+
@installed_version = installed_version
|
|
14
|
+
@target_version = target_version
|
|
15
|
+
@changes = changes.freeze
|
|
16
|
+
@blockers = blockers.freeze
|
|
17
|
+
@operation = operation
|
|
18
|
+
@applied = false
|
|
19
|
+
end
|
|
20
|
+
# rubocop:enable Metrics/ParameterLists
|
|
21
|
+
|
|
22
|
+
def safe? = blockers.empty?
|
|
23
|
+
def applied? = @applied
|
|
24
|
+
|
|
25
|
+
def apply!
|
|
26
|
+
raise MarketplaceError, 'Marketplace lifecycle plan was already applied' if applied?
|
|
27
|
+
raise MarketplaceError, "Marketplace lifecycle plan is blocked: #{blockers.join('; ')}" unless safe?
|
|
28
|
+
|
|
29
|
+
@operation.call
|
|
30
|
+
@applied = true
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Read-only view of a module package used to compare update boundaries.
|
|
36
|
+
class ModulePackageInventory
|
|
37
|
+
attr_reader :name, :version, :module_id, :unit_ids, :files
|
|
38
|
+
|
|
39
|
+
def initialize(name:, version:, module_id:, unit_ids:, files:)
|
|
40
|
+
@name = name
|
|
41
|
+
@version = version
|
|
42
|
+
@module_id = module_id
|
|
43
|
+
@unit_ids = unit_ids.freeze
|
|
44
|
+
@files = files.freeze
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
48
|
+
def self.read(path)
|
|
49
|
+
Dir.mktmpdir('mxrb-marketplace-inspect-') do |temporary|
|
|
50
|
+
Zip::File.open(path) do |archive|
|
|
51
|
+
reader = ModulePackageReader.new(archive)
|
|
52
|
+
descriptor = reader.descriptor
|
|
53
|
+
source_path = reader.extract_project(descriptor, temporary)
|
|
54
|
+
staged = reader.stage_files(descriptor.files, temporary)
|
|
55
|
+
source = IO::MprFile.open(source_path, readonly: true)
|
|
56
|
+
module_unit = find_module(source, descriptor.name)
|
|
57
|
+
raise MarketplaceError, "module #{descriptor.name.inspect} is absent from package MPR" unless module_unit
|
|
58
|
+
|
|
59
|
+
ids = subtree_ids(source, module_unit.fetch('UnitID'))
|
|
60
|
+
files = staged.transform_values { Digest::SHA256.file(_1).hexdigest }
|
|
61
|
+
return new(
|
|
62
|
+
name: descriptor.name, version: descriptor.version,
|
|
63
|
+
module_id: module_unit.fetch('UnitID'), unit_ids: ids, files:
|
|
64
|
+
)
|
|
65
|
+
ensure
|
|
66
|
+
source&.close
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
70
|
+
raise MarketplaceError, "invalid Mendix module package: #{e.message}"
|
|
71
|
+
end
|
|
72
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
73
|
+
|
|
74
|
+
def self.find_module(mpr, name)
|
|
75
|
+
mpr.units_by_containment('Modules').find { mpr.parse_contents(_1)['Name'] == name }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def self.subtree_ids(mpr, root_id)
|
|
79
|
+
ids = [root_id]
|
|
80
|
+
loop do
|
|
81
|
+
children = mpr.all_units.filter_map do |unit|
|
|
82
|
+
unit.fetch('UnitID') if ids.include?(unit['ContainerID']) && !ids.include?(unit.fetch('UnitID'))
|
|
83
|
+
end
|
|
84
|
+
break if children.empty?
|
|
85
|
+
|
|
86
|
+
ids.concat(children)
|
|
87
|
+
end
|
|
88
|
+
ids
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Transactional lifecycle for modules already recorded in marketplace.lock.json.
|
|
93
|
+
class Lifecycle # rubocop:disable Metrics/ClassLength
|
|
94
|
+
def initialize(target:, mpr:, installer:)
|
|
95
|
+
@target = File.expand_path(target)
|
|
96
|
+
@mpr = mpr && File.expand_path(mpr)
|
|
97
|
+
@installer = installer
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def installed(identifier)
|
|
101
|
+
pair = packages.find do |name, entry|
|
|
102
|
+
name.casecmp(identifier.to_s).zero? || entry['content_id'].to_s == identifier.to_s
|
|
103
|
+
end
|
|
104
|
+
raise MarketplaceError, "Marketplace package #{identifier.inspect} is not installed" unless pair
|
|
105
|
+
raise MarketplaceError, "Marketplace package #{pair.first.inspect} is not an imported module" \
|
|
106
|
+
unless pair.last['kind'] == 'module'
|
|
107
|
+
|
|
108
|
+
pair
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def mpr_path(entry)
|
|
112
|
+
path = safe_path(entry.fetch('destination'))
|
|
113
|
+
raise MarketplaceError, 'selected MPR does not match the Marketplace lock' if @mpr && @mpr != path
|
|
114
|
+
|
|
115
|
+
path
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
119
|
+
def plan_remove(identifier)
|
|
120
|
+
name, entry = installed(identifier)
|
|
121
|
+
mpr = mpr_path(entry)
|
|
122
|
+
ids = target_unit_ids(mpr, entry)
|
|
123
|
+
inventory = cached_inventory(entry)
|
|
124
|
+
shared = shared_files(name)
|
|
125
|
+
blockers = base_blockers(entry, inventory, ids)
|
|
126
|
+
blockers << "locked module name does not match cached package #{inventory.name}" if inventory.name != name
|
|
127
|
+
blockers.concat(external_reference_blockers(mpr, ids))
|
|
128
|
+
blockers.concat(modified_asset_blockers(inventory, name))
|
|
129
|
+
removable = inventory.files.keys - shared
|
|
130
|
+
changes = ["delete #{ids.size} MPR units", "delete #{removable.size} package assets",
|
|
131
|
+
'delete cached package and lock entry']
|
|
132
|
+
LifecyclePlan.new(
|
|
133
|
+
action: :remove, name:, installed_version: entry['version'], target_version: nil,
|
|
134
|
+
changes:, blockers:
|
|
135
|
+
) { apply_remove(name, entry, mpr, ids, removable) }
|
|
136
|
+
end
|
|
137
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
138
|
+
|
|
139
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
140
|
+
def plan_update(identifier, archive, package)
|
|
141
|
+
name, entry = installed(identifier)
|
|
142
|
+
mpr = mpr_path(entry)
|
|
143
|
+
current_ids = target_unit_ids(mpr, entry)
|
|
144
|
+
current = cached_inventory(entry)
|
|
145
|
+
replacement = ModulePackageInventory.read(archive)
|
|
146
|
+
missing_ids = current_ids - replacement.unit_ids
|
|
147
|
+
blockers = base_blockers(entry, current, current_ids)
|
|
148
|
+
blockers << "replacement module name changed from #{name} to #{replacement.name}" if replacement.name != name
|
|
149
|
+
blockers << 'replacement module identity changed' if replacement.module_id != entry['module_id']
|
|
150
|
+
blockers << "downloaded package version #{replacement.version} does not match #{package.version}" \
|
|
151
|
+
unless package_version_matches?(replacement.version, package.version)
|
|
152
|
+
blockers << "version #{package.version} is already installed" if package.version.to_s == entry['version'].to_s
|
|
153
|
+
blockers.concat(external_reference_blockers(mpr, missing_ids))
|
|
154
|
+
blockers.concat(modified_asset_blockers(current, name))
|
|
155
|
+
obsolete = current.files.keys - replacement.files.keys - shared_files(name)
|
|
156
|
+
changes = ["replace #{current_ids.size} MPR units with #{replacement.unit_ids.size}",
|
|
157
|
+
"install #{replacement.files.size} package assets",
|
|
158
|
+
"delete #{obsolete.size} obsolete package assets", 'replace cache and lock metadata']
|
|
159
|
+
LifecyclePlan.new(
|
|
160
|
+
action: :update, name:, installed_version: entry['version'], target_version: package.version,
|
|
161
|
+
changes:, blockers:
|
|
162
|
+
) { apply_update(entry, mpr, current_ids, obsolete, archive, package, replacement) }
|
|
163
|
+
end
|
|
164
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
165
|
+
|
|
166
|
+
private
|
|
167
|
+
|
|
168
|
+
def packages = OfficialMarketplace.lock(@target).fetch('packages')
|
|
169
|
+
|
|
170
|
+
def safe_path(relative)
|
|
171
|
+
path = File.expand_path(File.join(@target, relative.to_s))
|
|
172
|
+
prefix = "#{@target}#{File::SEPARATOR}"
|
|
173
|
+
raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix)
|
|
174
|
+
|
|
175
|
+
path
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def cached_inventory(entry)
|
|
179
|
+
archive = safe_path(entry.fetch('archive'))
|
|
180
|
+
raise MarketplaceError, "cached Marketplace package is missing: #{archive}" unless File.file?(archive)
|
|
181
|
+
|
|
182
|
+
ModulePackageInventory.read(archive)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def target_unit_ids(mpr_path, entry)
|
|
186
|
+
raise MarketplaceError, "MPR not found: #{mpr_path}" unless File.file?(mpr_path)
|
|
187
|
+
|
|
188
|
+
mpr = IO::MprFile.open(mpr_path, readonly: true)
|
|
189
|
+
root = mpr.unit(entry.fetch('module_id'))
|
|
190
|
+
raise MarketplaceError, "module #{entry.fetch('module_id')} is absent from target MPR" unless root
|
|
191
|
+
|
|
192
|
+
ModulePackageInventory.subtree_ids(mpr, root.fetch('UnitID'))
|
|
193
|
+
ensure
|
|
194
|
+
mpr&.close
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def base_blockers(entry, inventory, ids)
|
|
198
|
+
blockers = []
|
|
199
|
+
blockers << 'locked module identity does not match cached package' if inventory.module_id != entry['module_id']
|
|
200
|
+
blockers << 'locked unit count does not match target module tree' if entry['units'].to_i != ids.size
|
|
201
|
+
blockers
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
205
|
+
def external_reference_blockers(mpr_path, ids)
|
|
206
|
+
return [] if ids.empty?
|
|
207
|
+
|
|
208
|
+
id_set = ids.map(&:downcase)
|
|
209
|
+
mpr = IO::MprFile.open(mpr_path, readonly: true)
|
|
210
|
+
mpr.all_units.filter_map do |unit|
|
|
211
|
+
next if id_set.include?(unit.fetch('UnitID').downcase)
|
|
212
|
+
|
|
213
|
+
document = mpr.parse_contents(unit)
|
|
214
|
+
matches = id_set.select { JSON.generate(document).downcase.include?(_1) }
|
|
215
|
+
next if matches.empty?
|
|
216
|
+
|
|
217
|
+
"#{document['Name'] || document['$Type'] || unit.fetch('UnitID')} references " \
|
|
218
|
+
"#{matches.size} unit(s) scheduled for removal"
|
|
219
|
+
end
|
|
220
|
+
ensure
|
|
221
|
+
mpr&.close
|
|
222
|
+
end
|
|
223
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
224
|
+
|
|
225
|
+
def modified_asset_blockers(inventory, owner_name)
|
|
226
|
+
shared = shared_files(owner_name)
|
|
227
|
+
inventory.files.filter_map do |relative, expected|
|
|
228
|
+
next if shared.include?(relative)
|
|
229
|
+
|
|
230
|
+
path = safe_path(relative)
|
|
231
|
+
actual = Digest::SHA256.file(path).hexdigest if File.file?(path) && !File.symlink?(path)
|
|
232
|
+
"package asset changed or missing: #{relative}" unless actual == expected
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def shared_files(name)
|
|
237
|
+
packages.filter_map do |other_name, entry|
|
|
238
|
+
Array(entry['files']) unless other_name == name
|
|
239
|
+
end.flatten.uniq
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def package_version_matches?(manifest_version, official_version)
|
|
243
|
+
value = manifest_version.to_s
|
|
244
|
+
value.empty? || value == 'unknown' || value == official_version.to_s
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def apply_remove(name, entry, mpr, ids, files)
|
|
248
|
+
paths = lifecycle_paths(entry, mpr, files)
|
|
249
|
+
with_rollback(paths) do
|
|
250
|
+
delete_units(mpr, ids)
|
|
251
|
+
restore_assets(entry, files)
|
|
252
|
+
remove_atlas_variables if name == 'Atlas_Core'
|
|
253
|
+
FileUtils.rm_f(safe_path(entry.fetch('archive')))
|
|
254
|
+
delete_asset_backups(entry)
|
|
255
|
+
delete_lock_entry(name)
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
260
|
+
def apply_update(entry, mpr, ids, obsolete, archive, package, replacement)
|
|
261
|
+
new_cache = cache_path(replacement)
|
|
262
|
+
files = (Array(entry['files']) + replacement.files.keys).uniq
|
|
263
|
+
paths = lifecycle_paths(entry, mpr, files) + [new_cache]
|
|
264
|
+
with_rollback(paths.uniq) do
|
|
265
|
+
delete_units(mpr, ids)
|
|
266
|
+
@installer.send(:import_module, archive, package, mpr)
|
|
267
|
+
restore_assets(entry, obsolete)
|
|
268
|
+
delete_asset_backups(entry, obsolete)
|
|
269
|
+
old_cache = safe_path(entry.fetch('archive'))
|
|
270
|
+
FileUtils.rm_f(old_cache) unless old_cache == new_cache
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/ParameterLists
|
|
274
|
+
|
|
275
|
+
def delete_units(mpr_path, ids)
|
|
276
|
+
mpr = IO::MprFile.open(mpr_path)
|
|
277
|
+
mpr.transaction { ids.reverse_each { mpr.delete_unit(_1) } }
|
|
278
|
+
ensure
|
|
279
|
+
mpr&.close
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def lifecycle_paths(entry, mpr, files)
|
|
283
|
+
contents = File.join(File.dirname(mpr), 'mprcontents')
|
|
284
|
+
lock = File.join(@target, '.mxrb', 'marketplace.lock.json')
|
|
285
|
+
custom = File.join(@target, 'theme', 'web', 'custom-variables.scss')
|
|
286
|
+
originals = File.join(@target, '.mxrb', 'marketplace-originals')
|
|
287
|
+
[mpr, contents, lock, custom, originals, safe_path(entry.fetch('archive'))] + files.map { safe_path(_1) }
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def cache_path(inventory)
|
|
291
|
+
version = inventory.version.to_s.gsub(/[^A-Za-z0-9_.-]/, '_')
|
|
292
|
+
File.join(@target, '.mxrb', 'marketplace', "#{inventory.name}-#{version}.mpk")
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def with_rollback(paths)
|
|
296
|
+
Dir.mktmpdir('mxrb-marketplace-rollback-') do |temporary|
|
|
297
|
+
snapshots = []
|
|
298
|
+
paths.uniq.each_with_index { |path, index| snapshots << snapshot(path, temporary, index) }
|
|
299
|
+
yield
|
|
300
|
+
rescue StandardError
|
|
301
|
+
snapshots.reverse_each { restore_snapshot(_1) }
|
|
302
|
+
raise
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def snapshot(path, temporary, index)
|
|
307
|
+
backup = File.join(temporary, index.to_s)
|
|
308
|
+
type = snapshot_type(path)
|
|
309
|
+
FileUtils.cp_r(path, backup) if type == :directory
|
|
310
|
+
FileUtils.cp(path, backup) if type == :file
|
|
311
|
+
[path, backup, type]
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def snapshot_type(path)
|
|
315
|
+
return :directory if File.directory?(path)
|
|
316
|
+
return :file if File.file?(path)
|
|
317
|
+
|
|
318
|
+
:missing
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def restore_snapshot(snapshot)
|
|
322
|
+
path, backup, type = snapshot
|
|
323
|
+
FileUtils.rm_rf(path) if File.directory?(path)
|
|
324
|
+
FileUtils.rm_f(path) if File.file?(path)
|
|
325
|
+
return if type == :missing
|
|
326
|
+
|
|
327
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
328
|
+
type == :directory ? FileUtils.cp_r(backup, path) : FileUtils.cp(backup, path)
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def delete_lock_entry(name)
|
|
332
|
+
path = File.join(@target, '.mxrb', 'marketplace.lock.json')
|
|
333
|
+
lock = OfficialMarketplace.lock(@target)
|
|
334
|
+
lock.fetch('packages').delete(name)
|
|
335
|
+
@installer.send(:write_lock_atomically, path, lock)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def restore_assets(entry, files) # rubocop:disable Metrics/MethodLength
|
|
339
|
+
originals = entry.fetch('asset_originals', {})
|
|
340
|
+
files.each do |relative|
|
|
341
|
+
destination = safe_path(relative)
|
|
342
|
+
backup = originals[relative]
|
|
343
|
+
if backup && File.file?(safe_path(backup))
|
|
344
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
345
|
+
FileUtils.cp(safe_path(backup), destination)
|
|
346
|
+
else
|
|
347
|
+
FileUtils.rm_f(destination)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def delete_asset_backups(entry, files = nil)
|
|
353
|
+
originals = entry.fetch('asset_originals', {})
|
|
354
|
+
selected = files ? originals.slice(*files) : originals
|
|
355
|
+
selected.each_value { FileUtils.rm_f(safe_path(_1)) if _1 }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def remove_atlas_variables
|
|
359
|
+
path = File.join(@target, 'theme', 'web', 'custom-variables.scss')
|
|
360
|
+
return unless File.file?(path)
|
|
361
|
+
|
|
362
|
+
lines = File.readlines(path).reject { _1.strip == Installer::ATLAS_VARIABLES_IMPORT }
|
|
363
|
+
File.write(path, lines.join)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'rexml/document'
|
|
5
|
+
require 'tmpdir'
|
|
6
|
+
require 'zip'
|
|
7
|
+
|
|
8
|
+
module Mxrb
|
|
9
|
+
module OfficialMarketplace
|
|
10
|
+
# Parses and safely stages a Mendix module package archive.
|
|
11
|
+
class ModulePackageReader
|
|
12
|
+
Descriptor = Data.define(:name, :version, :model_version, :project_file, :files)
|
|
13
|
+
|
|
14
|
+
def initialize(archive)
|
|
15
|
+
@archive = archive
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def descriptor
|
|
19
|
+
package_xml = @archive.find_entry('package.xml')
|
|
20
|
+
raise MarketplaceError, 'package.xml is missing' unless package_xml
|
|
21
|
+
|
|
22
|
+
document = REXML::Document.new(package_xml.get_input_stream.read)
|
|
23
|
+
build_descriptor(document, parse_manifest)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def extract_project(descriptor, temporary)
|
|
27
|
+
entry = @archive.find_entry(descriptor.project_file)
|
|
28
|
+
raise MarketplaceError, "package project not found: #{descriptor.project_file}" unless entry
|
|
29
|
+
|
|
30
|
+
copy_entry(entry, File.join(temporary, 'source.mpr'))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stage_files(files, temporary)
|
|
34
|
+
root = File.join(temporary, 'assets')
|
|
35
|
+
files.to_h do |relative|
|
|
36
|
+
entry = @archive.find_entry(relative) || @archive.find_entry(relative.tr('/', '\\'))
|
|
37
|
+
raise MarketplaceError, "declared package file is missing: #{relative}" unless entry&.file?
|
|
38
|
+
|
|
39
|
+
[relative, copy_entry(entry, File.join(root, relative))]
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# Package metadata is intentionally validated as one boundary.
|
|
46
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
47
|
+
def build_descriptor(document, manifest)
|
|
48
|
+
module_element = element(document, 'module')
|
|
49
|
+
project_element = element(document, 'projectFile')
|
|
50
|
+
raise MarketplaceError, 'package does not contain a Mendix module' unless module_element
|
|
51
|
+
raise MarketplaceError, 'package does not declare project.mpr' unless project_element
|
|
52
|
+
|
|
53
|
+
name = valid_module_name(module_element.attributes['name'])
|
|
54
|
+
validate_package_type!(manifest.dig('package', 'type'))
|
|
55
|
+
Descriptor.new(
|
|
56
|
+
name, manifest.dig('package', 'version') || 'unknown', manifest['model-version'],
|
|
57
|
+
safe_relative_path(project_element.attributes['path']),
|
|
58
|
+
elements(document, 'file').map { safe_relative_path(_1.attributes['path']) }.uniq
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
62
|
+
|
|
63
|
+
def valid_module_name(value)
|
|
64
|
+
name = value.to_s
|
|
65
|
+
return name if name.match?(/\A[A-Za-z][A-Za-z0-9_]*\z/)
|
|
66
|
+
|
|
67
|
+
raise MarketplaceError, "invalid module name #{name.inspect}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def validate_package_type!(value)
|
|
71
|
+
package_type = value.to_s
|
|
72
|
+
return if package_type.empty? || package_type.casecmp('Module').zero?
|
|
73
|
+
|
|
74
|
+
raise MarketplaceError, "unsupported marketplace package type #{package_type.inspect}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def parse_manifest
|
|
78
|
+
entry = @archive.find_entry('manifest.json')
|
|
79
|
+
entry ? JSON.parse(entry.get_input_stream.read) : {}
|
|
80
|
+
rescue JSON::ParserError => e
|
|
81
|
+
raise MarketplaceError, "invalid module manifest: #{e.message}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def elements(document, name)
|
|
85
|
+
found = []
|
|
86
|
+
document.root&.each_recursive { found << _1 if _1.is_a?(REXML::Element) && _1.name == name }
|
|
87
|
+
found
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def element(document, name) = elements(document, name).first
|
|
91
|
+
|
|
92
|
+
def safe_relative_path(value)
|
|
93
|
+
path = value.to_s.tr('\\', '/')
|
|
94
|
+
parts = path.split('/')
|
|
95
|
+
unsafe = path.empty? || path.start_with?('/') || parts.include?('..') || parts.include?('')
|
|
96
|
+
raise MarketplaceError, "unsafe package path #{value.inspect}" if unsafe
|
|
97
|
+
raise MarketplaceError, "protected package path #{value.inspect}" if protected_path?(parts)
|
|
98
|
+
|
|
99
|
+
parts.join('/')
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def protected_path?(parts)
|
|
103
|
+
%w[.git .mxrb mprcontents].include?(parts.first.downcase) ||
|
|
104
|
+
parts.last.downcase.end_with?('.mpr') && parts.length > 1
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def copy_entry(entry, destination)
|
|
108
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
109
|
+
File.open(destination, 'wb') { ::IO.copy_stream(entry.get_input_stream, _1) }
|
|
110
|
+
destination
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Installs declared package files with rollback support.
|
|
115
|
+
class ModulePackageAssets
|
|
116
|
+
def initialize(target_root, temporary)
|
|
117
|
+
@target_root = target_root
|
|
118
|
+
@backup_root = File.join(temporary, 'backups')
|
|
119
|
+
@changes = []
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def install(staged_files, protected_files = [])
|
|
123
|
+
staged_files.each do |relative, source|
|
|
124
|
+
install_file(relative, source) unless protected_files.include?(relative)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def rollback
|
|
129
|
+
@changes.reverse_each do |destination, backup|
|
|
130
|
+
backup ? FileUtils.cp(backup, destination) : FileUtils.rm_f(destination)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
private
|
|
135
|
+
|
|
136
|
+
def install_file(relative, source)
|
|
137
|
+
destination = safe_destination(relative)
|
|
138
|
+
raise MarketplaceError, "asset destination is a symbolic link: #{relative}" if File.symlink?(destination)
|
|
139
|
+
raise MarketplaceError, "asset destination is a directory: #{relative}" if File.directory?(destination)
|
|
140
|
+
|
|
141
|
+
backup = backup_file(relative, destination)
|
|
142
|
+
@changes << [destination, backup]
|
|
143
|
+
FileUtils.mkdir_p(File.dirname(destination))
|
|
144
|
+
FileUtils.cp(source, destination)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def safe_destination(relative)
|
|
148
|
+
destination = File.expand_path(File.join(@target_root, relative))
|
|
149
|
+
prefix = "#{@target_root}#{File::SEPARATOR}"
|
|
150
|
+
raise MarketplaceError, "unsafe asset destination: #{relative}" unless destination.start_with?(prefix)
|
|
151
|
+
|
|
152
|
+
destination
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def backup_file(relative, destination)
|
|
156
|
+
return unless File.file?(destination)
|
|
157
|
+
|
|
158
|
+
backup = File.join(@backup_root, relative)
|
|
159
|
+
FileUtils.mkdir_p(File.dirname(backup))
|
|
160
|
+
FileUtils.cp(destination, backup)
|
|
161
|
+
backup
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Imports a Mendix module package directly into an MPR without Studio Pro.
|
|
166
|
+
class ModulePackageImporter # rubocop:disable Metrics/ClassLength
|
|
167
|
+
Result = Data.define(
|
|
168
|
+
:module_name, :package_version, :module_id, :units, :files,
|
|
169
|
+
:source_version, :target_version
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def initialize(package_path, mpr_path, target_root: nil, allow_model_upgrade: false,
|
|
173
|
+
protected_files: [])
|
|
174
|
+
@package_path = File.expand_path(package_path)
|
|
175
|
+
@mpr_path = File.expand_path(mpr_path)
|
|
176
|
+
@target_root = File.expand_path(target_root || File.dirname(@mpr_path))
|
|
177
|
+
@allow_model_upgrade = allow_model_upgrade
|
|
178
|
+
@protected_files = protected_files
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def import!
|
|
182
|
+
validate_inputs!
|
|
183
|
+
Dir.mktmpdir('mxrb-module-import-') do |temporary|
|
|
184
|
+
Zip::File.open(@package_path) { import_archive(_1, temporary) }
|
|
185
|
+
end
|
|
186
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
187
|
+
raise MarketplaceError, "invalid Mendix module package: #{e.message}"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def import_archive(archive, temporary)
|
|
193
|
+
reader = ModulePackageReader.new(archive)
|
|
194
|
+
descriptor = reader.descriptor
|
|
195
|
+
source_path = reader.extract_project(descriptor, temporary)
|
|
196
|
+
staged_files = reader.stage_files(descriptor.files, temporary)
|
|
197
|
+
import_project(source_path, descriptor, staged_files, temporary)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def validate_inputs!
|
|
201
|
+
raise MarketplaceError, "package not found: #{@package_path}" unless File.file?(@package_path)
|
|
202
|
+
raise MarketplaceError, "MPR not found: #{@mpr_path}" unless File.file?(@mpr_path)
|
|
203
|
+
raise MarketplaceError, "target root not found: #{@target_root}" unless File.directory?(@target_root)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def import_project(source_path, descriptor, staged_files, temporary) # rubocop:disable Metrics/MethodLength
|
|
207
|
+
source = IO::MprFile.open(source_path, readonly: true)
|
|
208
|
+
target = IO::MprFile.open(@mpr_path)
|
|
209
|
+
assets = ModulePackageAssets.new(@target_root, temporary)
|
|
210
|
+
imported_ids = []
|
|
211
|
+
import_transaction(source, target, descriptor, staged_files, assets, imported_ids)
|
|
212
|
+
import_result(source, target, descriptor, staged_files, imported_ids)
|
|
213
|
+
rescue StandardError
|
|
214
|
+
assets&.rollback
|
|
215
|
+
cleanup_v2_units(target, imported_ids)
|
|
216
|
+
raise
|
|
217
|
+
ensure
|
|
218
|
+
source&.close
|
|
219
|
+
target&.close
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# rubocop:disable Metrics/ParameterLists
|
|
223
|
+
def import_transaction(source, target, descriptor, staged_files, assets, imported_ids)
|
|
224
|
+
validate_versions!(source, target, descriptor)
|
|
225
|
+
module_unit, units = package_units(source, descriptor.name)
|
|
226
|
+
validate_target!(target, descriptor.name, units)
|
|
227
|
+
target.transaction do
|
|
228
|
+
imported_ids.concat(insert_units(source, target, module_unit, units))
|
|
229
|
+
assets.install(staged_files, @protected_files)
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
# rubocop:enable Metrics/ParameterLists
|
|
233
|
+
|
|
234
|
+
def insert_units(source, target, module_unit, units)
|
|
235
|
+
units.map do |unit|
|
|
236
|
+
container = unit == module_unit ? target.root_unit.fetch('UnitID') : unit.fetch('ContainerID')
|
|
237
|
+
target.insert_unit(
|
|
238
|
+
container_uuid: container, containment_name: unit.fetch('ContainmentName'),
|
|
239
|
+
contents_doc: source.parse_contents(unit)
|
|
240
|
+
)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def import_result(source, target, descriptor, staged_files, imported_ids)
|
|
245
|
+
Result.new(
|
|
246
|
+
descriptor.name, descriptor.version, imported_ids.first, imported_ids.size,
|
|
247
|
+
staged_files.keys, source.mendix_version, target.mendix_version
|
|
248
|
+
)
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def validate_versions!(source, target, descriptor)
|
|
252
|
+
package_version = descriptor.model_version.to_s
|
|
253
|
+
if !package_version.empty? && package_version != source.mendix_version
|
|
254
|
+
raise MarketplaceError,
|
|
255
|
+
"module manifest version #{package_version} does not match package MPR #{source.mendix_version}"
|
|
256
|
+
end
|
|
257
|
+
return if source.mendix_version == target.mendix_version
|
|
258
|
+
return if @allow_model_upgrade && forward_version?(source.mendix_version, target.mendix_version)
|
|
259
|
+
|
|
260
|
+
raise MarketplaceError,
|
|
261
|
+
"module uses Mendix #{source.mendix_version}, target uses #{target.mendix_version}; " \
|
|
262
|
+
'pure-Ruby import requires matching model versions'
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def forward_version?(source, target)
|
|
266
|
+
Gem::Version.new(numeric_version(target)) >= Gem::Version.new(numeric_version(source))
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def numeric_version(value)
|
|
270
|
+
value.to_s[/\A\d+(?:\.\d+){0,3}/] || '0'
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def package_units(source, module_name) # rubocop:disable Metrics/MethodLength
|
|
274
|
+
module_unit = find_module_unit(source, module_name)
|
|
275
|
+
raise MarketplaceError, "module #{module_name.inspect} is absent from package MPR" unless module_unit
|
|
276
|
+
|
|
277
|
+
units = [module_unit]
|
|
278
|
+
loop do
|
|
279
|
+
ids = units.map { _1.fetch('UnitID') }
|
|
280
|
+
children = source.all_units.select do |unit|
|
|
281
|
+
ids.include?(unit['ContainerID']) && !ids.include?(unit['UnitID'])
|
|
282
|
+
end
|
|
283
|
+
break if children.empty?
|
|
284
|
+
|
|
285
|
+
units.concat(children)
|
|
286
|
+
end
|
|
287
|
+
[module_unit, units]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def find_module_unit(source, module_name)
|
|
291
|
+
source.units_by_containment('Modules').find do |unit|
|
|
292
|
+
document = source.parse_contents(unit)
|
|
293
|
+
%w[Projects$ModuleImpl Projects$Module].include?(document['$Type']) &&
|
|
294
|
+
document['Name'] == module_name
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def validate_target!(target, module_name, units)
|
|
299
|
+
duplicate = target.units_by_containment('Modules').any? do |unit|
|
|
300
|
+
target.parse_contents(unit)['Name'] == module_name
|
|
301
|
+
end
|
|
302
|
+
raise MarketplaceError, "module #{module_name} already exists in target MPR" if duplicate
|
|
303
|
+
|
|
304
|
+
existing_ids = target.all_units.to_h { [_1.fetch('UnitID'), true] }
|
|
305
|
+
collision = units.find { existing_ids[_1.fetch('UnitID')] }
|
|
306
|
+
return unless collision
|
|
307
|
+
|
|
308
|
+
raise MarketplaceError, "module unit ID already exists: #{collision.fetch('UnitID')}"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def cleanup_v2_units(target, ids)
|
|
312
|
+
return unless target&.format_version == :v2
|
|
313
|
+
|
|
314
|
+
Array(ids).each { FileUtils.rm_f(target.content_path('UnitID' => _1)) }
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|