cumulusci-plus 5.0.21__py3-none-any.whl → 5.0.43__py3-none-any.whl
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.
- cumulusci/__about__.py +1 -1
- cumulusci/cli/logger.py +2 -2
- cumulusci/cli/service.py +20 -0
- cumulusci/cli/task.py +19 -3
- cumulusci/cli/tests/test_error.py +3 -1
- cumulusci/cli/tests/test_flow.py +279 -2
- cumulusci/cli/tests/test_org.py +5 -0
- cumulusci/cli/tests/test_service.py +15 -12
- cumulusci/cli/tests/test_task.py +122 -2
- cumulusci/cli/tests/utils.py +1 -4
- cumulusci/core/config/__init__.py +1 -0
- cumulusci/core/config/base_task_flow_config.py +26 -1
- cumulusci/core/config/org_config.py +2 -1
- cumulusci/core/config/project_config.py +14 -20
- cumulusci/core/config/scratch_org_config.py +12 -0
- cumulusci/core/config/tests/test_config.py +1 -0
- cumulusci/core/config/tests/test_config_expensive.py +9 -3
- cumulusci/core/config/universal_config.py +3 -4
- cumulusci/core/dependencies/base.py +5 -1
- cumulusci/core/dependencies/dependencies.py +1 -1
- cumulusci/core/dependencies/github.py +1 -2
- cumulusci/core/dependencies/resolvers.py +1 -1
- cumulusci/core/dependencies/tests/test_dependencies.py +1 -1
- cumulusci/core/dependencies/tests/test_resolvers.py +1 -1
- cumulusci/core/flowrunner.py +90 -6
- cumulusci/core/github.py +1 -1
- cumulusci/core/sfdx.py +3 -1
- cumulusci/core/source_transforms/tests/test_transforms.py +1 -1
- cumulusci/core/source_transforms/transforms.py +1 -1
- cumulusci/core/tasks.py +13 -2
- cumulusci/core/tests/test_flowrunner.py +100 -0
- cumulusci/core/tests/test_tasks.py +65 -0
- cumulusci/core/utils.py +3 -1
- cumulusci/core/versions.py +1 -1
- cumulusci/cumulusci.yml +73 -1
- cumulusci/oauth/client.py +1 -1
- cumulusci/plugins/plugin_base.py +5 -3
- cumulusci/robotframework/pageobjects/ObjectManagerPageObject.py +1 -1
- cumulusci/salesforce_api/rest_deploy.py +1 -1
- cumulusci/schema/cumulusci.jsonschema.json +69 -0
- cumulusci/tasks/apex/anon.py +1 -1
- cumulusci/tasks/apex/testrunner.py +421 -144
- cumulusci/tasks/apex/tests/test_apex_tasks.py +917 -1
- cumulusci/tasks/bulkdata/extract.py +0 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/synthesize_extract_declarations.py +1 -1
- cumulusci/tasks/bulkdata/extract_dataset_utils/tests/test_extract_yml.py +1 -1
- cumulusci/tasks/bulkdata/generate_and_load_data.py +136 -12
- cumulusci/tasks/bulkdata/mapping_parser.py +139 -44
- cumulusci/tasks/bulkdata/select_utils.py +1 -1
- cumulusci/tasks/bulkdata/snowfakery.py +100 -25
- cumulusci/tasks/bulkdata/tests/test_generate_and_load.py +159 -0
- cumulusci/tasks/bulkdata/tests/test_load.py +0 -2
- cumulusci/tasks/bulkdata/tests/test_mapping_parser.py +763 -1
- cumulusci/tasks/bulkdata/tests/test_select_utils.py +46 -0
- cumulusci/tasks/bulkdata/tests/test_snowfakery.py +133 -0
- cumulusci/tasks/create_package_version.py +190 -16
- cumulusci/tasks/datadictionary.py +1 -1
- cumulusci/tasks/metadata_etl/__init__.py +2 -0
- cumulusci/tasks/metadata_etl/applications.py +256 -0
- cumulusci/tasks/metadata_etl/base.py +7 -3
- cumulusci/tasks/metadata_etl/layouts.py +1 -1
- cumulusci/tasks/metadata_etl/permissions.py +1 -1
- cumulusci/tasks/metadata_etl/remote_site_settings.py +2 -2
- cumulusci/tasks/metadata_etl/tests/test_applications.py +710 -0
- cumulusci/tasks/push/README.md +15 -17
- cumulusci/tasks/release_notes/README.md +13 -13
- cumulusci/tasks/release_notes/generator.py +13 -8
- cumulusci/tasks/robotframework/tests/test_robotframework.py +6 -1
- cumulusci/tasks/salesforce/Deploy.py +53 -2
- cumulusci/tasks/salesforce/SfPackageCommands.py +363 -0
- cumulusci/tasks/salesforce/__init__.py +1 -0
- cumulusci/tasks/salesforce/assign_ps_psg.py +448 -0
- cumulusci/tasks/salesforce/composite.py +1 -1
- cumulusci/tasks/salesforce/custom_settings_wait.py +1 -1
- cumulusci/tasks/salesforce/enable_prediction.py +5 -1
- cumulusci/tasks/salesforce/getPackageVersion.py +89 -0
- cumulusci/tasks/salesforce/insert_record.py +18 -19
- cumulusci/tasks/salesforce/sourcetracking.py +1 -1
- cumulusci/tasks/salesforce/tests/test_Deploy.py +316 -1
- cumulusci/tasks/salesforce/tests/test_SfPackageCommands.py +554 -0
- cumulusci/tasks/salesforce/tests/test_assign_ps_psg.py +1055 -0
- cumulusci/tasks/salesforce/tests/test_enable_prediction.py +4 -2
- cumulusci/tasks/salesforce/tests/test_getPackageVersion.py +651 -0
- cumulusci/tasks/salesforce/tests/test_update_dependencies.py +1 -1
- cumulusci/tasks/salesforce/tests/test_update_external_auth_identity_provider.py +927 -0
- cumulusci/tasks/salesforce/tests/test_update_external_credential.py +1427 -0
- cumulusci/tasks/salesforce/tests/test_update_named_credential.py +1042 -0
- cumulusci/tasks/salesforce/tests/test_update_record.py +512 -0
- cumulusci/tasks/salesforce/update_dependencies.py +2 -2
- cumulusci/tasks/salesforce/update_external_auth_identity_provider.py +551 -0
- cumulusci/tasks/salesforce/update_external_credential.py +647 -0
- cumulusci/tasks/salesforce/update_named_credential.py +441 -0
- cumulusci/tasks/salesforce/update_profile.py +17 -13
- cumulusci/tasks/salesforce/update_record.py +217 -0
- cumulusci/tasks/salesforce/users/permsets.py +62 -5
- cumulusci/tasks/salesforce/users/tests/test_permsets.py +237 -11
- cumulusci/tasks/sfdmu/__init__.py +0 -0
- cumulusci/tasks/sfdmu/sfdmu.py +376 -0
- cumulusci/tasks/sfdmu/tests/__init__.py +1 -0
- cumulusci/tasks/sfdmu/tests/test_runner.py +212 -0
- cumulusci/tasks/sfdmu/tests/test_sfdmu.py +1012 -0
- cumulusci/tasks/tests/test_create_package_version.py +716 -1
- cumulusci/tasks/tests/test_util.py +42 -0
- cumulusci/tasks/util.py +37 -1
- cumulusci/tasks/utility/copyContents.py +402 -0
- cumulusci/tasks/utility/credentialManager.py +302 -0
- cumulusci/tasks/utility/directoryRecreator.py +30 -0
- cumulusci/tasks/utility/env_management.py +1 -1
- cumulusci/tasks/utility/secretsToEnv.py +135 -0
- cumulusci/tasks/utility/tests/test_copyContents.py +1719 -0
- cumulusci/tasks/utility/tests/test_credentialManager.py +1150 -0
- cumulusci/tasks/utility/tests/test_directoryRecreator.py +439 -0
- cumulusci/tasks/utility/tests/test_secretsToEnv.py +1118 -0
- cumulusci/tests/test_integration_infrastructure.py +3 -1
- cumulusci/tests/test_utils.py +70 -6
- cumulusci/utils/__init__.py +54 -9
- cumulusci/utils/classutils.py +5 -2
- cumulusci/utils/http/tests/cassettes/ManualEditTestCompositeParallelSalesforce.test_http_headers.yaml +31 -30
- cumulusci/utils/options.py +23 -1
- cumulusci/utils/parallel/task_worker_queues/parallel_worker.py +1 -1
- cumulusci/utils/yaml/cumulusci_yml.py +8 -3
- cumulusci/utils/yaml/model_parser.py +2 -2
- cumulusci/utils/yaml/tests/test_cumulusci_yml.py +1 -1
- cumulusci/utils/yaml/tests/test_model_parser.py +3 -3
- cumulusci/vcs/base.py +23 -15
- cumulusci/vcs/bootstrap.py +5 -4
- cumulusci/vcs/utils/list_modified_files.py +189 -0
- cumulusci/vcs/utils/tests/test_list_modified_files.py +588 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/METADATA +11 -10
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/RECORD +135 -104
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/WHEEL +1 -1
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/entry_points.txt +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/AUTHORS.rst +0 -0
- {cumulusci_plus-5.0.21.dist-info → cumulusci_plus-5.0.43.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
from pydantic.v1 import root_validator
|
|
5
|
+
|
|
6
|
+
from cumulusci.core.exceptions import SalesforceDXException
|
|
7
|
+
from cumulusci.salesforce_api.utils import get_simple_salesforce_connection
|
|
8
|
+
from cumulusci.tasks.salesforce import BaseSalesforceApiTask
|
|
9
|
+
from cumulusci.utils.options import CCIOptions, Field
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NamedCredentialCalloutOptions(CCIOptions):
|
|
13
|
+
# Callout options
|
|
14
|
+
allow_merge_fields_in_body: bool = Field(
|
|
15
|
+
None,
|
|
16
|
+
description="Allow merge fields in body. [default to None]",
|
|
17
|
+
)
|
|
18
|
+
allow_merge_fields_in_header: bool = Field(
|
|
19
|
+
None,
|
|
20
|
+
description="Allow merge fields in header. [default to None]",
|
|
21
|
+
)
|
|
22
|
+
generate_authorization_header: bool = Field(
|
|
23
|
+
None,
|
|
24
|
+
description="Generate authorization header. [default to None]",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class NamedCredentialHttpHeader(CCIOptions):
|
|
29
|
+
name: str = Field(
|
|
30
|
+
None,
|
|
31
|
+
description="Name. [default to None]",
|
|
32
|
+
)
|
|
33
|
+
value: str = Field(
|
|
34
|
+
None,
|
|
35
|
+
description="Value. [default to None]",
|
|
36
|
+
)
|
|
37
|
+
sequence_number: int = Field(
|
|
38
|
+
None,
|
|
39
|
+
description="Sequence number. [default to None]",
|
|
40
|
+
)
|
|
41
|
+
secret: bool = Field(
|
|
42
|
+
False,
|
|
43
|
+
description="Is the value a secret. [default to False]",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class NamedCredentialParameter(CCIOptions):
|
|
48
|
+
allowed_managed_package_namespaces: str = Field(
|
|
49
|
+
None,
|
|
50
|
+
description="Allowed managed package namespaces. [default to None]",
|
|
51
|
+
)
|
|
52
|
+
url: str = Field(
|
|
53
|
+
None,
|
|
54
|
+
description="Url. [default to None]",
|
|
55
|
+
)
|
|
56
|
+
authentication: str = Field(
|
|
57
|
+
None,
|
|
58
|
+
description="Authentication. [default to None]",
|
|
59
|
+
)
|
|
60
|
+
certificate: str = Field(
|
|
61
|
+
None,
|
|
62
|
+
description="Certificate. [default to None]",
|
|
63
|
+
)
|
|
64
|
+
http_header: List["NamedCredentialHttpHeader"] = Field(
|
|
65
|
+
[],
|
|
66
|
+
description="Http header. [default to empty list]",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@root_validator
|
|
70
|
+
def check_parameters(cls, values):
|
|
71
|
+
"""Check if only one of the parameters is provided"""
|
|
72
|
+
if (
|
|
73
|
+
len(
|
|
74
|
+
([values.get("url")] if values.get("url") else [])
|
|
75
|
+
+ (
|
|
76
|
+
[values.get("allowed_managed_package_namespaces")]
|
|
77
|
+
if values.get("allowed_managed_package_namespaces")
|
|
78
|
+
else []
|
|
79
|
+
)
|
|
80
|
+
+ (
|
|
81
|
+
[values.get("authentication")]
|
|
82
|
+
if values.get("authentication")
|
|
83
|
+
else []
|
|
84
|
+
)
|
|
85
|
+
+ ([values.get("certificate")] if values.get("certificate") else [])
|
|
86
|
+
+ (
|
|
87
|
+
[len(values.get("http_header"))]
|
|
88
|
+
if values.get("http_header")
|
|
89
|
+
else []
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
== 1
|
|
93
|
+
):
|
|
94
|
+
return values
|
|
95
|
+
raise ValueError("Only one of the parameters is required.")
|
|
96
|
+
|
|
97
|
+
def param_type(self):
|
|
98
|
+
"""Get the parameter type"""
|
|
99
|
+
if self.url:
|
|
100
|
+
return "Url"
|
|
101
|
+
if self.authentication:
|
|
102
|
+
return "Authentication"
|
|
103
|
+
if self.certificate:
|
|
104
|
+
return "ClientCertificate"
|
|
105
|
+
if self.allowed_managed_package_namespaces:
|
|
106
|
+
return "AllowedManagedPackageNamespaces"
|
|
107
|
+
if self.http_header:
|
|
108
|
+
return "HttpHeader"
|
|
109
|
+
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
def param_value(self, http_header=None):
|
|
113
|
+
"""Get the parameter value"""
|
|
114
|
+
if self.url:
|
|
115
|
+
return self.url
|
|
116
|
+
if self.authentication:
|
|
117
|
+
return self.authentication
|
|
118
|
+
if self.certificate:
|
|
119
|
+
return self.certificate
|
|
120
|
+
if self.allowed_managed_package_namespaces:
|
|
121
|
+
return self.allowed_managed_package_namespaces
|
|
122
|
+
if http_header:
|
|
123
|
+
http_header_item = next(
|
|
124
|
+
(item for item in self.http_header if item.name == http_header), None
|
|
125
|
+
)
|
|
126
|
+
if http_header_item:
|
|
127
|
+
return http_header_item.value
|
|
128
|
+
|
|
129
|
+
def get_parameter_to_update(self):
|
|
130
|
+
"""Get the parameter to update"""
|
|
131
|
+
ret = []
|
|
132
|
+
|
|
133
|
+
if len(self.http_header) > 0:
|
|
134
|
+
for http_header_item in self.http_header:
|
|
135
|
+
param_to_update = {}
|
|
136
|
+
param_to_update["parameterName"] = http_header_item.name
|
|
137
|
+
param_to_update["parameterType"] = "HttpHeader"
|
|
138
|
+
param_to_update["parameterValue"] = self.param_value(
|
|
139
|
+
http_header=http_header_item.name
|
|
140
|
+
)
|
|
141
|
+
param_to_update["secret"] = http_header_item.secret
|
|
142
|
+
|
|
143
|
+
if http_header_item.sequence_number is not None:
|
|
144
|
+
param_to_update["sequenceNumber"] = http_header_item.sequence_number
|
|
145
|
+
|
|
146
|
+
ret.append(param_to_update.copy())
|
|
147
|
+
else:
|
|
148
|
+
param_to_update = {}
|
|
149
|
+
param_to_update["parameterType"] = self.param_type()
|
|
150
|
+
param_to_update["parameterValue"] = self.param_value()
|
|
151
|
+
|
|
152
|
+
if param_to_update["parameterType"] == "ClientCertificate":
|
|
153
|
+
param_to_update["parameterName"] = "ClientCertificate"
|
|
154
|
+
param_to_update["certificate"] = param_to_update["parameterValue"]
|
|
155
|
+
|
|
156
|
+
if param_to_update["parameterType"] == "Authentication":
|
|
157
|
+
param_to_update["parameterName"] = "ExternalCredential"
|
|
158
|
+
param_to_update["externalCredential"] = param_to_update[
|
|
159
|
+
"parameterValue"
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
ret.append(param_to_update.copy())
|
|
163
|
+
|
|
164
|
+
return ret
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class TransformNamedCredentialParameter(NamedCredentialParameter):
|
|
168
|
+
def param_value(self, http_header=None):
|
|
169
|
+
value = super().param_value(http_header)
|
|
170
|
+
if value:
|
|
171
|
+
return os.getenv(value)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
NamedCredentialParameter.update_forward_refs()
|
|
176
|
+
TransformNamedCredentialParameter.update_forward_refs()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class UpdateNamedCredential(BaseSalesforceApiTask):
|
|
180
|
+
"""Custom task to update named credential parameters.
|
|
181
|
+
This task is based on the managability rules of named credentials.
|
|
182
|
+
https://developer.salesforce.com/docs/atlas.en-us.pkg2_dev.meta/pkg2_dev/packaging_packageable_components.htm#mdc_named_credential
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
class Options(CCIOptions):
|
|
186
|
+
name: str = Field(..., description="Name of the named credential to update.")
|
|
187
|
+
namespace: str = Field(
|
|
188
|
+
"",
|
|
189
|
+
description="Namespace of the named credential to update. [default to empty string]",
|
|
190
|
+
)
|
|
191
|
+
# Callout options
|
|
192
|
+
callout_options: NamedCredentialCalloutOptions = Field(
|
|
193
|
+
None,
|
|
194
|
+
description="Callout options. [default to None]",
|
|
195
|
+
)
|
|
196
|
+
# Named credential parameters
|
|
197
|
+
parameters: List[NamedCredentialParameter] = Field(
|
|
198
|
+
[],
|
|
199
|
+
description="Parameters to update. [default to empty list]",
|
|
200
|
+
)
|
|
201
|
+
# Named credential parameters
|
|
202
|
+
transform_parameters: List[TransformNamedCredentialParameter] = Field(
|
|
203
|
+
[],
|
|
204
|
+
description="Parameters to transform. [default to empty list]",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
parsed_options: Options
|
|
208
|
+
|
|
209
|
+
def _init_task(self):
|
|
210
|
+
self.tooling = get_simple_salesforce_connection(
|
|
211
|
+
self.project_config,
|
|
212
|
+
self.org_config,
|
|
213
|
+
api_version=self.project_config.project__package__api_version,
|
|
214
|
+
base_url="tooling",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
def _run_task(self):
|
|
218
|
+
# Step 1: Get the named credential id from the named credential name
|
|
219
|
+
named_credential_id = self._get_named_credential_id()
|
|
220
|
+
|
|
221
|
+
if not named_credential_id:
|
|
222
|
+
msg = f"Named credential '{self.parsed_options.name}' not found"
|
|
223
|
+
raise SalesforceDXException(msg)
|
|
224
|
+
|
|
225
|
+
# Step 2: Get the named credential object
|
|
226
|
+
named_credential = self._get_named_credential_object(named_credential_id)
|
|
227
|
+
|
|
228
|
+
if not named_credential:
|
|
229
|
+
msg = f"Failed to retrieve named credential object for '{self.parsed_options.name}'"
|
|
230
|
+
raise SalesforceDXException(msg)
|
|
231
|
+
|
|
232
|
+
if named_credential.get("namedCredentialType") != "SecuredEndpoint":
|
|
233
|
+
msg = f"Named credential '{self.parsed_options.name}' is not a secured endpoint, Aborting update. Only SecuredEndpoint is supported."
|
|
234
|
+
raise SalesforceDXException(msg)
|
|
235
|
+
|
|
236
|
+
# Step 3: Update the named credential parameters
|
|
237
|
+
self._update_named_credential_parameters(named_credential)
|
|
238
|
+
|
|
239
|
+
updated_named_credential = self._update_named_credential_object(
|
|
240
|
+
named_credential_id, named_credential
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not updated_named_credential:
|
|
244
|
+
msg = f"Failed to update named credential object for '{self.parsed_options.name}'"
|
|
245
|
+
raise SalesforceDXException(msg)
|
|
246
|
+
|
|
247
|
+
self.logger.info(
|
|
248
|
+
f"Successfully updated named credential '{self.parsed_options.name}'"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _get_named_credential_id(self) -> Optional[str]:
|
|
252
|
+
"""Get the named credential ID from the named credential name"""
|
|
253
|
+
query = f"SELECT Id FROM NamedCredential WHERE DeveloperName='{self.parsed_options.name}'"
|
|
254
|
+
|
|
255
|
+
if self.parsed_options.namespace:
|
|
256
|
+
query += f" AND NamespacePrefix='{self.parsed_options.namespace}'"
|
|
257
|
+
|
|
258
|
+
query += " LIMIT 1"
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
res = self.tooling.query(query)
|
|
262
|
+
if res["size"] == 0:
|
|
263
|
+
return None
|
|
264
|
+
return res["records"][0]["Id"]
|
|
265
|
+
except Exception as e:
|
|
266
|
+
self.logger.error(f"Error querying named credential: {str(e)}")
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
def _get_named_credential_object(
|
|
270
|
+
self, named_credential_id: str
|
|
271
|
+
) -> Optional[Dict[str, Any]]:
|
|
272
|
+
"""Get the named credential object using Metadata API"""
|
|
273
|
+
try:
|
|
274
|
+
# Use Tooling API to get the named credential metadata
|
|
275
|
+
result = self.tooling._call_salesforce(
|
|
276
|
+
method="GET",
|
|
277
|
+
url=f"{self.tooling.base_url}sobjects/NamedCredential/{named_credential_id}",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if result.status_code != 200:
|
|
281
|
+
self.logger.error(
|
|
282
|
+
f"Error retrieving named credential object: {result.json()}"
|
|
283
|
+
)
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
return result.json().get("Metadata", None)
|
|
287
|
+
|
|
288
|
+
except Exception as e:
|
|
289
|
+
self.logger.error(f"Error retrieving named credential object: {str(e)}")
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
def _update_named_credential_object(
|
|
293
|
+
self, named_credential_id: str, named_credential: Dict[str, Any]
|
|
294
|
+
) -> Optional[bool]:
|
|
295
|
+
"""Update the named credential object"""
|
|
296
|
+
try:
|
|
297
|
+
resultUpdate = self.tooling._call_salesforce(
|
|
298
|
+
method="PATCH",
|
|
299
|
+
url=f"{self.tooling.base_url}sobjects/NamedCredential/{named_credential_id}",
|
|
300
|
+
json={"Metadata": named_credential},
|
|
301
|
+
)
|
|
302
|
+
if not resultUpdate.ok:
|
|
303
|
+
self.logger.error(
|
|
304
|
+
f"Error updating named credential object: {resultUpdate.json()}"
|
|
305
|
+
)
|
|
306
|
+
return None
|
|
307
|
+
|
|
308
|
+
return resultUpdate.ok
|
|
309
|
+
except Exception as e:
|
|
310
|
+
raise SalesforceDXException(
|
|
311
|
+
f"Failed to update named credential object: {str(e)}"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
def _update_named_credential_parameters(self, named_credential: Dict[str, Any]):
|
|
315
|
+
"""Update the named credential parameters"""
|
|
316
|
+
try:
|
|
317
|
+
template_param = self._get_named_credential_template_parameter(
|
|
318
|
+
named_credential
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
self._update_callout_options(named_credential)
|
|
322
|
+
self._update_parameters(
|
|
323
|
+
named_credential, self.parsed_options.parameters, template_param
|
|
324
|
+
)
|
|
325
|
+
self._update_parameters(
|
|
326
|
+
named_credential,
|
|
327
|
+
self.parsed_options.transform_parameters,
|
|
328
|
+
template_param,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
raise SalesforceDXException(f"Failed to update parameters: {str(e)}")
|
|
333
|
+
|
|
334
|
+
def _update_callout_options(self, named_credential: Dict[str, Any]):
|
|
335
|
+
"""Update the callout options"""
|
|
336
|
+
if not self.parsed_options.callout_options:
|
|
337
|
+
return
|
|
338
|
+
|
|
339
|
+
if self.parsed_options.callout_options.allow_merge_fields_in_body:
|
|
340
|
+
named_credential[
|
|
341
|
+
"allowMergeFieldsInBody"
|
|
342
|
+
] = self.parsed_options.callout_options.allow_merge_fields_in_body
|
|
343
|
+
if self.parsed_options.callout_options.allow_merge_fields_in_header:
|
|
344
|
+
named_credential[
|
|
345
|
+
"allowMergeFieldsInHeader"
|
|
346
|
+
] = self.parsed_options.callout_options.allow_merge_fields_in_header
|
|
347
|
+
if self.parsed_options.callout_options.generate_authorization_header:
|
|
348
|
+
named_credential[
|
|
349
|
+
"generateAuthorizationHeader"
|
|
350
|
+
] = self.parsed_options.callout_options.generate_authorization_header
|
|
351
|
+
|
|
352
|
+
def _update_parameters(
|
|
353
|
+
self,
|
|
354
|
+
named_credential: Dict[str, Any],
|
|
355
|
+
named_credential_parameters: List[NamedCredentialParameter],
|
|
356
|
+
template_param: Dict[str, Any],
|
|
357
|
+
):
|
|
358
|
+
"""Update the parameters"""
|
|
359
|
+
for param_input in named_credential_parameters:
|
|
360
|
+
params_to_update = param_input.get_parameter_to_update()
|
|
361
|
+
|
|
362
|
+
for param_to_update in params_to_update:
|
|
363
|
+
secret = param_to_update.pop("secret", False)
|
|
364
|
+
|
|
365
|
+
param_to_update_copy = param_to_update.copy()
|
|
366
|
+
param_to_update_copy.pop("parameterValue", None)
|
|
367
|
+
param_to_update_copy.pop("certificate", None)
|
|
368
|
+
param_to_update_copy.pop("externalCredential", None)
|
|
369
|
+
|
|
370
|
+
cred_param = next(
|
|
371
|
+
(
|
|
372
|
+
param
|
|
373
|
+
for param in named_credential.get(
|
|
374
|
+
"namedCredentialParameters", []
|
|
375
|
+
)
|
|
376
|
+
if param_to_update_copy.items() <= param.items()
|
|
377
|
+
),
|
|
378
|
+
None,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if cred_param:
|
|
382
|
+
cred_param.update(param_to_update)
|
|
383
|
+
self.logger.info(
|
|
384
|
+
f"Updated parameter {cred_param['parameterType']}-{cred_param['parameterName']} with new value {param_to_update['parameterValue'] if not secret else '********'}"
|
|
385
|
+
)
|
|
386
|
+
else:
|
|
387
|
+
copy_template_param = template_param.copy()
|
|
388
|
+
copy_template_param.update(param_to_update)
|
|
389
|
+
named_credential["namedCredentialParameters"].append(
|
|
390
|
+
copy_template_param
|
|
391
|
+
)
|
|
392
|
+
self.logger.info(
|
|
393
|
+
f"Added parameter {copy_template_param['parameterType']}-{copy_template_param['parameterName']} with new value {param_to_update['parameterValue'] if not secret else '********'}"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def _update_parameter_value(
|
|
397
|
+
self, named_credential_parameter: Dict[str, Any], new_value: str
|
|
398
|
+
):
|
|
399
|
+
"""Update an existing parameter's value"""
|
|
400
|
+
update_data = {"parameterValue": new_value}
|
|
401
|
+
named_credential_parameter.update(update_data)
|
|
402
|
+
|
|
403
|
+
def _get_named_credential_template_parameter(
|
|
404
|
+
self, named_credential: Dict[str, Any]
|
|
405
|
+
) -> Dict[str, Any]:
|
|
406
|
+
"""Get the named credential template parameter"""
|
|
407
|
+
template_param = [
|
|
408
|
+
param
|
|
409
|
+
for param in named_credential.get("namedCredentialParameters", [])
|
|
410
|
+
if param.get("parameterType") == "Url"
|
|
411
|
+
]
|
|
412
|
+
if len(template_param) == 0:
|
|
413
|
+
self.logger.warning(
|
|
414
|
+
f"No template parameter found for named credential '{self.parsed_options.name}', using default template parameter."
|
|
415
|
+
)
|
|
416
|
+
return {
|
|
417
|
+
"certificate": None,
|
|
418
|
+
"description": None,
|
|
419
|
+
"externalCredential": None,
|
|
420
|
+
"globalNamedPrincipalCredential": None,
|
|
421
|
+
"managedFeatureEnabledCallout": None,
|
|
422
|
+
"outboundNetworkConnection": None,
|
|
423
|
+
"parameterName": None,
|
|
424
|
+
"parameterType": None,
|
|
425
|
+
"parameterValue": None,
|
|
426
|
+
"readOnlyNamedCredential": None,
|
|
427
|
+
"sequenceNumber": None,
|
|
428
|
+
"systemUserNamedCredential": None,
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
ret = template_param[0].copy()
|
|
432
|
+
ret.update(
|
|
433
|
+
{
|
|
434
|
+
"parameterName": None,
|
|
435
|
+
"parameterType": None,
|
|
436
|
+
"parameterValue": None,
|
|
437
|
+
"sequenceNumber": None,
|
|
438
|
+
}
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
return ret
|
|
@@ -231,28 +231,28 @@ class ProfileGrantAllAccess(MetadataSingleEntityTransformTask, BaseSalesforceApi
|
|
|
231
231
|
|
|
232
232
|
def _strip_namespace_from_record_type(self, record_type):
|
|
233
233
|
"""Strip namespace prefix from record type string.
|
|
234
|
-
|
|
234
|
+
|
|
235
235
|
Converts 'namespace__Object__c.namespace__RecordType' to 'Object__c.RecordType'
|
|
236
236
|
"""
|
|
237
237
|
if not self.options.get("namespace_inject"):
|
|
238
238
|
return record_type
|
|
239
|
-
|
|
239
|
+
|
|
240
240
|
namespace = self.options["namespace_inject"]
|
|
241
241
|
namespace_prefix = f"{namespace}__"
|
|
242
|
-
|
|
242
|
+
|
|
243
243
|
# Split on the dot to handle object and record type separately
|
|
244
244
|
parts = record_type.split(".", 1)
|
|
245
245
|
if len(parts) != 2:
|
|
246
246
|
return record_type
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
object_name, record_type_name = parts
|
|
249
|
-
|
|
249
|
+
|
|
250
250
|
# Strip namespace prefix from both object and record type if present
|
|
251
251
|
if object_name.startswith(namespace_prefix):
|
|
252
|
-
object_name = object_name[len(namespace_prefix):]
|
|
252
|
+
object_name = object_name[len(namespace_prefix) :]
|
|
253
253
|
if record_type_name.startswith(namespace_prefix):
|
|
254
|
-
record_type_name = record_type_name[len(namespace_prefix):]
|
|
255
|
-
|
|
254
|
+
record_type_name = record_type_name[len(namespace_prefix) :]
|
|
255
|
+
|
|
256
256
|
return f"{object_name}.{record_type_name}"
|
|
257
257
|
|
|
258
258
|
def _set_record_types(self, tree, api_name):
|
|
@@ -287,13 +287,17 @@ class ProfileGrantAllAccess(MetadataSingleEntityTransformTask, BaseSalesforceApi
|
|
|
287
287
|
# Look for the recordTypeVisibilities element
|
|
288
288
|
# First try with the original record type
|
|
289
289
|
elem = tree.find("recordTypeVisibilities", recordType=rt["record_type"])
|
|
290
|
-
|
|
291
|
-
# If not found and we're in a namespaced org, try with namespace stripped
|
|
290
|
+
|
|
291
|
+
# If not found and we're in a namespaced org, try with namespace stripped
|
|
292
292
|
# (since Salesforce may return without namespace in namespaced orgs)
|
|
293
293
|
if elem is None and self.options.get("namespaced_org"):
|
|
294
|
-
record_type_without_namespace = self._strip_namespace_from_record_type(
|
|
295
|
-
|
|
296
|
-
|
|
294
|
+
record_type_without_namespace = self._strip_namespace_from_record_type(
|
|
295
|
+
rt["record_type"]
|
|
296
|
+
)
|
|
297
|
+
elem = tree.find(
|
|
298
|
+
"recordTypeVisibilities", recordType=record_type_without_namespace
|
|
299
|
+
)
|
|
300
|
+
|
|
297
301
|
if elem is None:
|
|
298
302
|
raise TaskOptionsError(
|
|
299
303
|
f"Record Type {rt['record_type']} (or {record_type_without_namespace}) not found in retrieved {api_name}.profile"
|