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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
# Adds an application module to an existing Ruby-first project atomically.
|
|
5
|
+
class ModuleInitializer < Initializer
|
|
6
|
+
Result = Data.define(:root, :files, :project_file)
|
|
7
|
+
|
|
8
|
+
def initialize(name)
|
|
9
|
+
super(name, subject: :module)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def scaffold(into: Dir.pwd)
|
|
13
|
+
prepare(into)
|
|
14
|
+
install
|
|
15
|
+
Result.new(@module_root, module_files, @project_file)
|
|
16
|
+
ensure
|
|
17
|
+
cleanup
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def prepare(into)
|
|
23
|
+
@project_root = File.expand_path(into)
|
|
24
|
+
@project_file = File.join(@project_root, 'project.rb')
|
|
25
|
+
raise ArgumentError, "#{@project_file}: project.rb not found" unless File.file?(@project_file)
|
|
26
|
+
|
|
27
|
+
@module_root = File.join(@project_root, 'modules', @module_name)
|
|
28
|
+
abort "#{@module_root}: directory already exists" if File.exist?(@module_root)
|
|
29
|
+
|
|
30
|
+
@staging = Dir.mktmpdir(".#{@module_name}.mxrb-module-", @project_root)
|
|
31
|
+
@project_staging = File.join(@project_root, ".project.rb.mxrb-#{Process.pid}")
|
|
32
|
+
write_module_scaffold(@staging)
|
|
33
|
+
stage_project
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def stage_project
|
|
37
|
+
source = File.binread(@project_file)
|
|
38
|
+
File.binwrite(@project_staging, connect_module(source))
|
|
39
|
+
File.chmod(File.stat(@project_file).mode, @project_staging)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def install
|
|
43
|
+
FileUtils.mkdir_p(File.dirname(@module_root))
|
|
44
|
+
File.rename(@staging, @module_root)
|
|
45
|
+
@module_installed = true
|
|
46
|
+
File.rename(@project_staging, @project_file)
|
|
47
|
+
@completed = true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def module_files
|
|
51
|
+
relative_module_files.map { File.join(@module_root, _1) }.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def connect_module(source)
|
|
55
|
+
raise ArgumentError, 'project.rb does not contain Mxrb.define' unless source.include?('Mxrb.define')
|
|
56
|
+
|
|
57
|
+
load_line = %(evaluate File.join(__dir__, "modules", "#{@module_name}", "module.rb"))
|
|
58
|
+
raise ArgumentError, "project.rb already loads #{@module_name}" if source.include?(load_line)
|
|
59
|
+
|
|
60
|
+
lines = source.lines
|
|
61
|
+
closing = lines.rindex { _1.strip == 'end' }
|
|
62
|
+
raise ArgumentError, 'project.rb does not contain a closing end' unless closing
|
|
63
|
+
|
|
64
|
+
indentation = lines.filter_map { |line| line[/\A\s+(?=evaluate File\.join\(__dir__, "modules")/] }.first || ' '
|
|
65
|
+
lines.insert(closing, "#{indentation}#{load_line}\n")
|
|
66
|
+
lines.join
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def cleanup
|
|
70
|
+
FileUtils.rm_rf(@module_root) if @module_installed && !@completed
|
|
71
|
+
FileUtils.rm_rf(@staging) if @staging && File.exist?(@staging)
|
|
72
|
+
FileUtils.rm_f(@project_staging) if @project_staging && File.exist?(@project_staging)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
module OfficialMarketplace
|
|
5
|
+
# Client and resolver for the documented Mendix Marketplace Content API.
|
|
6
|
+
class ContentApi # rubocop:disable Metrics/ClassLength
|
|
7
|
+
BASE_URL = 'https://marketplace-api.mendix.com/v1'
|
|
8
|
+
AUTHORIZED_HOSTS = %w[marketplace-api.mendix.com marketplace.mendix.com].freeze
|
|
9
|
+
CONTENT_LIMIT = 100
|
|
10
|
+
VERSION_LIMIT = 20
|
|
11
|
+
MENDIX_VERSION = /\A(?:\d{1,3}|1000)(?:\.(?:\d{1,3}|1000)){0,2}\z/
|
|
12
|
+
MENDIX_BUILD_VERSION = /\A(?:\d{1,3}|1000)(?:\.(?:\d{1,3}|1000)){2}\.\d{1,10}\z/
|
|
13
|
+
UUID = /\A[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}\z/i
|
|
14
|
+
|
|
15
|
+
def initialize(pat: nil, credentials: Credentials.new, client: HttpClient.new)
|
|
16
|
+
@pat = pat.to_s.strip
|
|
17
|
+
@pat = ENV['MXRB_MENDIX_PAT'].to_s.strip if @pat.empty?
|
|
18
|
+
@pat = credentials.pat.to_s.strip if @pat.empty?
|
|
19
|
+
raise MarketplaceError, missing_pat_message if @pat.empty?
|
|
20
|
+
|
|
21
|
+
@client = client
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def search(name: nil, private: nil, approved: nil, published_since: nil, # rubocop:disable Metrics/ParameterLists
|
|
25
|
+
limit: 10, offset: 0)
|
|
26
|
+
parameters = {
|
|
27
|
+
name:, isPrivate: boolean_filter(private), isCompanyApproved: boolean_filter(approved),
|
|
28
|
+
publishedSince: date_filter(published_since), limit: bounded_integer(limit, 1..CONTENT_LIMIT),
|
|
29
|
+
offset: bounded_integer(offset, 0..)
|
|
30
|
+
}.compact
|
|
31
|
+
items(request('/content', parameters))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def content(content_id)
|
|
35
|
+
identifier = positive_integer(content_id, 'content ID')
|
|
36
|
+
request("/content/#{identifier}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def versions(content_id, version_id: nil, mendix_version: nil, published_since: nil, # rubocop:disable Metrics/ParameterLists
|
|
40
|
+
limit: VERSION_LIMIT, offset: 0)
|
|
41
|
+
identifier = positive_integer(content_id, 'content ID')
|
|
42
|
+
parameters = {
|
|
43
|
+
versionId: uuid_filter(version_id),
|
|
44
|
+
supportedMendixVersion: mendix_version_filter(mendix_version),
|
|
45
|
+
publishedSince: date_filter(published_since),
|
|
46
|
+
limit: bounded_integer(limit, 1..VERSION_LIMIT), offset: bounded_integer(offset, 0..)
|
|
47
|
+
}.compact
|
|
48
|
+
items(request("/content/#{identifier}/versions", parameters))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def find(identifier)
|
|
52
|
+
return content(identifier) if identifier.to_s.match?(/\A\d+\z/)
|
|
53
|
+
|
|
54
|
+
candidates(identifier).each do |name|
|
|
55
|
+
matches = search(name:)
|
|
56
|
+
return matches.first if matches.one?
|
|
57
|
+
raise MarketplaceError, "Marketplace content name #{name.inspect} is ambiguous" \
|
|
58
|
+
if matches.length > 1
|
|
59
|
+
end
|
|
60
|
+
raise MarketplaceError, "Marketplace content not found: #{identifier.inspect}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def resolve(identifier, version: nil, mendix_version: nil, allow_vulnerable: false)
|
|
64
|
+
selected_content = find(identifier)
|
|
65
|
+
selected_version = resolve_version(selected_content.fetch('contentId'), version, mendix_version)
|
|
66
|
+
ensure_compatible!(selected_version, mendix_version) if mendix_version
|
|
67
|
+
reject_vulnerable!(selected_version) unless allow_vulnerable
|
|
68
|
+
package(selected_content, selected_version)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def download(version_id, destination, download_url: nil)
|
|
72
|
+
identifier = uuid_filter(version_id)
|
|
73
|
+
url = download_url || "#{BASE_URL}/versions/#{identifier}/download"
|
|
74
|
+
uri = URI.parse(url)
|
|
75
|
+
trusted = AUTHORIZED_HOSTS.include?(uri.host)
|
|
76
|
+
@client.download(url, destination, authorization: trusted ? authorization : nil)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def all_versions(content_id, published_since: nil)
|
|
80
|
+
paginated(VERSION_LIMIT) do |limit, offset|
|
|
81
|
+
versions(content_id, published_since:, limit:, offset:)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def request(path, parameters = {})
|
|
88
|
+
@client.json(url(path, parameters), authorization: authorization)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def authorization = "MxToken #{@pat}"
|
|
92
|
+
|
|
93
|
+
def url(path, parameters)
|
|
94
|
+
query = URI.encode_www_form(parameters)
|
|
95
|
+
query.empty? ? "#{BASE_URL}#{path}" : "#{BASE_URL}#{path}?#{query}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def items(payload)
|
|
99
|
+
value = payload.fetch('items', [])
|
|
100
|
+
raise MarketplaceError, 'invalid Marketplace Content API response: items must be an array' \
|
|
101
|
+
unless value.is_a?(Array)
|
|
102
|
+
|
|
103
|
+
value
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def resolve_version(content_id, version, mendix_version)
|
|
107
|
+
values = version_candidates(content_id, version, mendix_version)
|
|
108
|
+
raise MarketplaceError, version_not_found_message(version, mendix_version) if values.empty?
|
|
109
|
+
|
|
110
|
+
values.max_by { version_key(_1) }
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def version_candidates(content_id, version, mendix_version)
|
|
114
|
+
return versions(content_id, version_id: version) if version&.match?(UUID)
|
|
115
|
+
return all_versions(content_id).select { _1['versionNumber'].to_s == version } if version
|
|
116
|
+
return versions(content_id, mendix_version:) if mendix_version
|
|
117
|
+
|
|
118
|
+
versions(content_id)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def package(content, version)
|
|
122
|
+
OfficialPackage.new(
|
|
123
|
+
version['name'] || content.dig('latestVersion', 'name') || "Content#{content['contentId']}",
|
|
124
|
+
version.fetch('versionNumber'), :mendix, version['downloadURL'] || version['downloadUrl'],
|
|
125
|
+
"content:#{content.fetch('contentId')}", content.fetch('contentId'),
|
|
126
|
+
version.fetch('versionId'), content.fetch('type'), version['versionType'] || 'Regular',
|
|
127
|
+
security_codes(version), content['isPrivate'], content['isCompanyApproved']
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def reject_vulnerable!(version)
|
|
132
|
+
return unless version['versionType'] == 'Vulnerable'
|
|
133
|
+
|
|
134
|
+
codes = security_codes(version)
|
|
135
|
+
suffix = codes.empty? ? '' : " (#{codes.join(', ')})"
|
|
136
|
+
raise MarketplaceError,
|
|
137
|
+
"Marketplace version #{version['versionNumber']} is marked vulnerable#{suffix}; " \
|
|
138
|
+
'select a security fix or pass --allow-vulnerable'
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def ensure_compatible!(version, mendix_version)
|
|
142
|
+
minimum = version['minSupportedMendixVersion'].to_s
|
|
143
|
+
compatible = !minimum.empty? && Gem::Version.new(mendix_version) >= Gem::Version.new(minimum)
|
|
144
|
+
return if compatible
|
|
145
|
+
|
|
146
|
+
raise MarketplaceError,
|
|
147
|
+
"Marketplace version #{version['versionNumber']} requires Mendix #{minimum} or newer"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def security_codes(version)
|
|
151
|
+
issues = Array(version['vulnerabilities']) + Array(version['fixedSecurityIssues'])
|
|
152
|
+
issues.filter_map { _1['code'] }.uniq
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def candidates(identifier)
|
|
156
|
+
name = identifier.to_s.strip
|
|
157
|
+
raise MarketplaceError, 'Marketplace content name must not be empty' if name.empty?
|
|
158
|
+
|
|
159
|
+
spaced = name.gsub(/([a-z\d])([A-Z])/, '\\1 \\2').tr('_-', ' ').squeeze(' ')
|
|
160
|
+
[name, spaced].uniq
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def paginated(page_size)
|
|
164
|
+
offset = 0
|
|
165
|
+
result = []
|
|
166
|
+
loop do
|
|
167
|
+
page = yield(page_size, offset)
|
|
168
|
+
result.concat(page)
|
|
169
|
+
break if page.length < page_size
|
|
170
|
+
|
|
171
|
+
offset += page_size
|
|
172
|
+
end
|
|
173
|
+
result
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def version_key(value)
|
|
177
|
+
[value['publicationDate'].to_s, Gem::Version.new(normalize_version(value['versionNumber']))]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def normalize_version(value)
|
|
181
|
+
value.to_s[/\A\d+(?:\.\d+){0,2}/] || '0'
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def boolean_filter(value)
|
|
185
|
+
return if value.nil?
|
|
186
|
+
return value.to_s if [true, false].include?(value)
|
|
187
|
+
return value if %w[true false].include?(value)
|
|
188
|
+
|
|
189
|
+
raise MarketplaceError, "expected true or false, got #{value.inspect}"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def date_filter(value)
|
|
193
|
+
return if value.nil?
|
|
194
|
+
|
|
195
|
+
Date.iso8601(value.to_s).iso8601
|
|
196
|
+
rescue Date::Error
|
|
197
|
+
raise MarketplaceError, "invalid RFC 3339 full date #{value.inspect}"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def uuid_filter(value)
|
|
201
|
+
return if value.nil?
|
|
202
|
+
raise MarketplaceError, "invalid Marketplace version ID #{value.inspect}" unless value.to_s.match?(UUID)
|
|
203
|
+
|
|
204
|
+
value.to_s
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def mendix_version_filter(value)
|
|
208
|
+
return if value.nil?
|
|
209
|
+
|
|
210
|
+
valid = value.to_s.match?(MENDIX_VERSION) || value.to_s.match?(MENDIX_BUILD_VERSION)
|
|
211
|
+
raise MarketplaceError, "invalid Mendix version #{value.inspect}" unless valid
|
|
212
|
+
|
|
213
|
+
value.to_s.split('.').first(3).join('.')
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def bounded_integer(value, range)
|
|
217
|
+
integer = Integer(value)
|
|
218
|
+
raise MarketplaceError, "value #{value.inspect} is outside #{range}" unless range.cover?(integer)
|
|
219
|
+
|
|
220
|
+
integer
|
|
221
|
+
rescue ArgumentError, TypeError
|
|
222
|
+
raise MarketplaceError, "expected an integer, got #{value.inspect}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def positive_integer(value, label)
|
|
226
|
+
integer = bounded_integer(value, 1..)
|
|
227
|
+
integer.to_s
|
|
228
|
+
rescue MarketplaceError
|
|
229
|
+
raise MarketplaceError, "invalid #{label} #{value.inspect}"
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def version_not_found_message(version, mendix_version)
|
|
233
|
+
qualifier = if version
|
|
234
|
+
"version #{version.inspect}"
|
|
235
|
+
elsif mendix_version
|
|
236
|
+
"a version compatible with Mendix #{mendix_version}"
|
|
237
|
+
else
|
|
238
|
+
'a published version'
|
|
239
|
+
end
|
|
240
|
+
"Marketplace content does not have #{qualifier}"
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def missing_pat_message
|
|
244
|
+
'Mendix PAT not found; run `mxrb marketplace login --help` and configure a PAT file ' \
|
|
245
|
+
'or Ruby .env with scope mx:marketplace-content:read'
|
|
246
|
+
end
|
|
247
|
+
end # rubocop:enable Metrics/ClassLength
|
|
248
|
+
|
|
249
|
+
AuditResult = Data.define(
|
|
250
|
+
:name, :installed_version, :latest_version, :version_type, :issues, :outdated, :valid
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Reviews Content API-backed lock entries for updates and known vulnerabilities.
|
|
254
|
+
class SecurityAuditor
|
|
255
|
+
def initialize(target:, api: nil)
|
|
256
|
+
@target = File.expand_path(target)
|
|
257
|
+
@api = api
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def audit(mendix_version: nil)
|
|
261
|
+
entries = OfficialMarketplace.lock(@target).fetch('packages').select do |_name, entry|
|
|
262
|
+
entry['source'] == 'mendix' && entry['content_id']
|
|
263
|
+
end
|
|
264
|
+
return [] if entries.empty?
|
|
265
|
+
|
|
266
|
+
@api ||= ContentApi.new
|
|
267
|
+
entries.map do |name, entry|
|
|
268
|
+
audit_entry(name, entry, mendix_version)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def audit_entry(name, entry, mendix_version)
|
|
275
|
+
installed = @api.versions(entry.fetch('content_id'), version_id: entry['version_id']).first
|
|
276
|
+
latest = @api.resolve(entry.fetch('content_id').to_s, mendix_version:, allow_vulnerable: true)
|
|
277
|
+
type, issues, installed_version = installed_metadata(installed, entry)
|
|
278
|
+
AuditResult.new(
|
|
279
|
+
name, installed_version, latest.version, type, issues,
|
|
280
|
+
installed_version != latest.version, !installed.nil? && type != 'Vulnerable'
|
|
281
|
+
)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def installed_metadata(installed, entry)
|
|
285
|
+
type = installed&.fetch('versionType', nil) || 'Unknown'
|
|
286
|
+
issues = Array(installed&.fetch('vulnerabilities', nil)).filter_map { _1['code'] }
|
|
287
|
+
version = installed&.fetch('versionNumber', nil) || entry['version']
|
|
288
|
+
[type, issues, version]
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mxrb
|
|
4
|
+
module OfficialMarketplace
|
|
5
|
+
Dependency = Data.define(:module_name, :package, :archive, :dependencies)
|
|
6
|
+
WidgetDependency = Data.define(:package, :archive, :widget_ids, :required_ids)
|
|
7
|
+
|
|
8
|
+
# Preview for a recursively verified dependency installation.
|
|
9
|
+
class DependencyPlan
|
|
10
|
+
attr_reader :root, :dependencies, :widget_dependencies, :blockers
|
|
11
|
+
|
|
12
|
+
def initialize(root:, dependencies:, blockers:, widget_dependencies: [], &operation)
|
|
13
|
+
@root = root
|
|
14
|
+
@dependencies = dependencies.freeze
|
|
15
|
+
@widget_dependencies = widget_dependencies.freeze
|
|
16
|
+
@blockers = blockers.freeze
|
|
17
|
+
@operation = operation
|
|
18
|
+
@applied = false
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def safe? = blockers.empty?
|
|
22
|
+
def applied? = @applied
|
|
23
|
+
|
|
24
|
+
def changes
|
|
25
|
+
dependencies.map { "install #{_1.module_name} #{_1.package.version}" } +
|
|
26
|
+
widget_dependencies.map { "install widget bundle #{_1.package.name} #{_1.package.version}" }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def apply!
|
|
30
|
+
raise MarketplaceError, 'Marketplace dependency plan was already applied' if applied?
|
|
31
|
+
raise MarketplaceError, "Marketplace dependency plan is blocked: #{blockers.join('; ')}" unless safe?
|
|
32
|
+
raise MarketplaceError, 'Marketplace dependency preview expired; rerun with apply enabled' unless @operation
|
|
33
|
+
|
|
34
|
+
apply_operation
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def apply_resolved!
|
|
38
|
+
raise MarketplaceError, 'Marketplace dependency plan was already applied' if applied?
|
|
39
|
+
raise MarketplaceError, 'Marketplace dependency preview expired; rerun with apply enabled' unless @operation
|
|
40
|
+
|
|
41
|
+
apply_operation
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def apply_operation
|
|
45
|
+
@operation.call
|
|
46
|
+
@applied = true
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def expire!
|
|
51
|
+
@operation = nil unless applied?
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Discovers dependencies from unresolved qualified references inside the
|
|
57
|
+
# package MPR, then verifies Content API candidates against the MPK module.
|
|
58
|
+
class DependencyResolver # rubocop:disable Metrics/ClassLength
|
|
59
|
+
PLATFORM_MODULES = %w[System].freeze
|
|
60
|
+
OFFICIAL_CONTENT_IDS = {
|
|
61
|
+
'FeedbackModule' => 205_506,
|
|
62
|
+
'DataWidgets' => 116_540,
|
|
63
|
+
'Administration' => 23_513
|
|
64
|
+
}.freeze
|
|
65
|
+
DATA_WIDGET_PATTERN = /
|
|
66
|
+
\Acom\.mendix\.widget\.web\.
|
|
67
|
+
(?:datagrid|datagriddatefilter|datagriddropdownfilter|datagridnumberfilter|
|
|
68
|
+
datagridtextfilter|dropdownsort|gallery|selectionhelper|treenode)\.
|
|
69
|
+
/ix
|
|
70
|
+
OFFICIAL_WIDGET_CONTENT_IDS = {
|
|
71
|
+
DATA_WIDGET_PATTERN => 116_540,
|
|
72
|
+
/\Acom\.mendix\.widget\.web\.combobox\.Combobox\z/i => 219_304
|
|
73
|
+
}.freeze
|
|
74
|
+
|
|
75
|
+
def initialize(target:, mpr:, installer:, api:, mendix_version:)
|
|
76
|
+
@target = File.expand_path(target)
|
|
77
|
+
@mpr = File.expand_path(mpr)
|
|
78
|
+
@installer = installer
|
|
79
|
+
@api = api
|
|
80
|
+
@mendix_version = mendix_version
|
|
81
|
+
@resolved = {}
|
|
82
|
+
@visited = {}
|
|
83
|
+
@blockers = []
|
|
84
|
+
@project_modules = Mxrb.open(@mpr) { _1.modules.map(&:name) }
|
|
85
|
+
@required_widget_ids = widget_ids(@mpr)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
89
|
+
def resolve(identifier, apply: false, apply_resolved: false)
|
|
90
|
+
lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: @installer)
|
|
91
|
+
root, entry = lifecycle.installed(identifier)
|
|
92
|
+
Dir.mktmpdir('mxrb-marketplace-dependencies-') do |temporary|
|
|
93
|
+
@temporary = temporary
|
|
94
|
+
visit(root, safe_path(entry.fetch('archive')))
|
|
95
|
+
dependencies = @resolved.values.freeze
|
|
96
|
+
widget_dependencies = resolve_widget_dependencies
|
|
97
|
+
plan = DependencyPlan.new(
|
|
98
|
+
root:, dependencies:, widget_dependencies:, blockers: @blockers.uniq
|
|
99
|
+
) do
|
|
100
|
+
apply_dependencies(dependencies, widget_dependencies)
|
|
101
|
+
end
|
|
102
|
+
plan.apply! if apply
|
|
103
|
+
plan.apply_resolved! if apply_resolved
|
|
104
|
+
plan.expire! unless apply || apply_resolved
|
|
105
|
+
return plan
|
|
106
|
+
end
|
|
107
|
+
ensure
|
|
108
|
+
@temporary = nil
|
|
109
|
+
end
|
|
110
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
115
|
+
def visit(module_name, archive)
|
|
116
|
+
return if @visited[module_name]
|
|
117
|
+
|
|
118
|
+
@visited[module_name] = true
|
|
119
|
+
dependency_names(archive).each do |dependency_name|
|
|
120
|
+
next if PLATFORM_MODULES.include?(dependency_name) || dependency_name == module_name
|
|
121
|
+
next if @project_modules.include?(dependency_name) && !packages.key?(dependency_name)
|
|
122
|
+
|
|
123
|
+
dependency = installed_dependency(dependency_name) || resolve_dependency(dependency_name)
|
|
124
|
+
next unless dependency
|
|
125
|
+
|
|
126
|
+
visit(dependency.module_name, dependency.archive)
|
|
127
|
+
@resolved[dependency.module_name] ||= dependency unless installed?(dependency.module_name)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
131
|
+
|
|
132
|
+
def dependency_names(archive)
|
|
133
|
+
Dir.mktmpdir('mxrb-marketplace-dependency-model-') do |temporary|
|
|
134
|
+
project = extract_project(archive, temporary)
|
|
135
|
+
@required_widget_ids.concat(widget_ids(project)).uniq!
|
|
136
|
+
Mxrb.open(project) do |model|
|
|
137
|
+
return model.analyze.unresolved_references.filter_map do |reference|
|
|
138
|
+
reference.qualified_name.to_s.split(%r{[./]}, 2).first
|
|
139
|
+
end.uniq.sort
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def extract_project(archive_path, temporary)
|
|
145
|
+
Zip::File.open(archive_path) do |archive|
|
|
146
|
+
reader = ModulePackageReader.new(archive)
|
|
147
|
+
descriptor = reader.descriptor
|
|
148
|
+
reader.extract_project(descriptor, temporary)
|
|
149
|
+
end
|
|
150
|
+
rescue Zip::Error, REXML::ParseException => e
|
|
151
|
+
raise MarketplaceError, "invalid Mendix dependency package: #{e.message}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def installed_dependency(name)
|
|
155
|
+
pair = packages.find { |module_name, _entry| module_name.casecmp(name).zero? }
|
|
156
|
+
return unless pair
|
|
157
|
+
|
|
158
|
+
module_name, entry = pair
|
|
159
|
+
Dependency.new(module_name, nil, safe_path(entry.fetch('archive')), dependency_names_for(entry))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def dependency_names_for(entry)
|
|
163
|
+
Array(entry['dependencies'])
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def resolve_dependency(name) # rubocop:disable Metrics/MethodLength
|
|
167
|
+
failures = []
|
|
168
|
+
candidates(name).each do |content|
|
|
169
|
+
package = resolve_candidate(content)
|
|
170
|
+
next unless package
|
|
171
|
+
|
|
172
|
+
archive = download_candidate(package)
|
|
173
|
+
inventory = ModulePackageInventory.read(archive)
|
|
174
|
+
next unless inventory.name == name
|
|
175
|
+
|
|
176
|
+
dependencies = dependency_names(archive)
|
|
177
|
+
return Dependency.new(name, package, archive, dependencies)
|
|
178
|
+
rescue MarketplaceError => e
|
|
179
|
+
failures << "#{name}: candidate #{content['contentId']} failed: #{e.message}"
|
|
180
|
+
end
|
|
181
|
+
@blockers.concat(failures)
|
|
182
|
+
@blockers << "#{name}: no official Marketplace package matched the module identity"
|
|
183
|
+
nil
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def candidates(name)
|
|
187
|
+
(official_candidates(name) + queries(name).flat_map { @api.search(name: _1, limit: 100) })
|
|
188
|
+
.select { Installer::IMPORTABLE_CONTENT_TYPES.include?(_1['type']) }
|
|
189
|
+
.uniq { _1['contentId'] }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def official_candidates(name)
|
|
193
|
+
pair = OFFICIAL_CONTENT_IDS.find { |module_name, _content_id| module_name.casecmp(name).zero? }
|
|
194
|
+
pair ? [@api.content(pair.last)] : []
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def queries(name)
|
|
198
|
+
humanized = name.gsub('_', ' ').gsub(/([a-z\d])([A-Z])/, '\\1 \\2')
|
|
199
|
+
[humanized, name].uniq
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def resolve_candidate(content)
|
|
203
|
+
@api.resolve(content.fetch('contentId'), mendix_version: @mendix_version)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def download_candidate(package)
|
|
207
|
+
destination = File.join(@temporary, "#{package.content_id}-#{package.version_id}.mpk")
|
|
208
|
+
return destination if File.file?(destination)
|
|
209
|
+
|
|
210
|
+
@api.download(package.version_id, destination, download_url: package.download_url)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def resolve_widget_dependencies # rubocop:disable Metrics/MethodLength
|
|
214
|
+
missing = @required_widget_ids.reject { widget_installed?(_1) }
|
|
215
|
+
grouped = missing.group_by { official_widget_content_id(_1) }
|
|
216
|
+
Array(grouped.delete(nil)).each do |widget_id|
|
|
217
|
+
@blockers << "#{widget_id}: no verified official Marketplace widget mapping"
|
|
218
|
+
end
|
|
219
|
+
grouped.filter_map do |content_id, required_ids|
|
|
220
|
+
resolve_widget_dependency(content_id, required_ids)
|
|
221
|
+
rescue MarketplaceError => e
|
|
222
|
+
@blockers << "widget content #{content_id}: #{e.message}"
|
|
223
|
+
nil
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
228
|
+
def resolve_widget_dependency(content_id, required_ids)
|
|
229
|
+
if package_installed?(content_id)
|
|
230
|
+
raise MarketplaceError,
|
|
231
|
+
"locked official package is present but does not provide #{required_ids.join(', ')}"
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
existing = @resolved.values.find { _1.package&.content_id == content_id }
|
|
235
|
+
package = existing&.package || @api.resolve(content_id, mendix_version: @mendix_version)
|
|
236
|
+
@installer.validate_official_package!(package)
|
|
237
|
+
archive = existing&.archive || download_candidate(package)
|
|
238
|
+
inventory = WidgetBundleInventory.read(archive)
|
|
239
|
+
missing = required_ids - inventory.widget_ids
|
|
240
|
+
unless missing.empty?
|
|
241
|
+
raise MarketplaceError,
|
|
242
|
+
"downloaded package does not provide #{missing.join(', ')}"
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
WidgetDependency.new(package, archive, inventory.widget_ids, required_ids.sort)
|
|
246
|
+
end
|
|
247
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
248
|
+
|
|
249
|
+
def official_widget_content_id(widget_id)
|
|
250
|
+
pair = OFFICIAL_WIDGET_CONTENT_IDS.find { |pattern, _content_id| pattern.match?(widget_id) }
|
|
251
|
+
pair&.last
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def widget_installed?(widget_id)
|
|
255
|
+
!Mxrb::WidgetPackage.find(@target, widget_id).nil?
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def widget_ids(mpr_path)
|
|
259
|
+
Mxrb.open(mpr_path) do |model|
|
|
260
|
+
model.all_units.flat_map do |unit|
|
|
261
|
+
collect_widget_ids(model.parse_bson(unit))
|
|
262
|
+
end.uniq.sort
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def collect_widget_ids(value)
|
|
267
|
+
case value
|
|
268
|
+
when Hash
|
|
269
|
+
own = value['WidgetId'].to_s
|
|
270
|
+
nested = value.values.flat_map { collect_widget_ids(_1) }
|
|
271
|
+
own.empty? ? nested : [own] + nested
|
|
272
|
+
when Array
|
|
273
|
+
value.flat_map { collect_widget_ids(_1) }
|
|
274
|
+
else
|
|
275
|
+
[]
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def apply_dependencies(dependencies, widget_dependencies) # rubocop:disable Metrics/MethodLength
|
|
280
|
+
paths = rollback_paths(dependencies, widget_dependencies)
|
|
281
|
+
lifecycle = Lifecycle.new(target: @target, mpr: @mpr, installer: @installer)
|
|
282
|
+
lifecycle.send(:with_rollback, paths) do
|
|
283
|
+
dependencies.each do |dependency|
|
|
284
|
+
@installer.send(:import_module, dependency.archive, dependency.package, @mpr)
|
|
285
|
+
end
|
|
286
|
+
widget_dependencies.each do |dependency|
|
|
287
|
+
next if package_installed?(dependency.package.content_id)
|
|
288
|
+
|
|
289
|
+
@installer.send(:install_official_archive, dependency.archive, dependency.package, @mpr)
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def rollback_paths(dependencies, widget_dependencies)
|
|
295
|
+
archives = (dependencies + widget_dependencies).map(&:archive)
|
|
296
|
+
assets = archives.flat_map { package_asset_paths(_1) }
|
|
297
|
+
[
|
|
298
|
+
@mpr, File.join(File.dirname(@mpr), 'mprcontents'),
|
|
299
|
+
File.join(@target, '.mxrb', 'marketplace.lock.json'),
|
|
300
|
+
File.join(@target, '.mxrb', 'marketplace'),
|
|
301
|
+
File.join(@target, '.mxrb', 'marketplace-originals'),
|
|
302
|
+
File.join(@target, 'theme', 'web', 'custom-variables.scss')
|
|
303
|
+
] + assets.map { safe_path(_1) }
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def package_asset_paths(archive)
|
|
307
|
+
return [File.join('widgets', WidgetPackageInventory.read(archive).project_filename)] \
|
|
308
|
+
if PackageEnvelope.kind(archive) == :widget
|
|
309
|
+
|
|
310
|
+
ModulePackageInventory.read(archive).files.keys
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def package_installed?(content_id)
|
|
314
|
+
packages.any? { |_name, entry| entry['content_id'].to_s == content_id.to_s }
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def packages = OfficialMarketplace.lock(@target).fetch('packages')
|
|
318
|
+
def installed?(name) = packages.key?(name)
|
|
319
|
+
|
|
320
|
+
def safe_path(relative)
|
|
321
|
+
path = File.expand_path(File.join(@target, relative.to_s))
|
|
322
|
+
prefix = "#{@target}#{File::SEPARATOR}"
|
|
323
|
+
raise MarketplaceError, "path is outside marketplace target: #{path}" unless path.start_with?(prefix)
|
|
324
|
+
|
|
325
|
+
path
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|