This is an automated email from the ASF dual-hosted git repository.
SbloodyS pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler.git
The following commit(s) were added to refs/heads/dev by this push:
new eca5d5e3df [Fix-18389][DataX] Read job definition from attached
resource file when custom json is empty (#18434)
eca5d5e3df is described below
commit eca5d5e3df52f812c7820b5da6de5feb5cc140e4
Author: Nikhil Ramashasthri <[email protected]>
AuthorDate: Mon Aug 31 02:39:46 2026 -0400
[Fix-18389][DataX] Read job definition from attached resource file when
custom json is empty (#18434)
---
.../plugin/task/datax/DataxParameters.java | 49 +++++++-
.../plugin/task/datax/DataxTask.java | 32 +++++-
.../plugin/task/datax/DataxParametersTest.java | 102 +++++++++++++++++
.../plugin/task/datax/DataxTaskTest.java | 124 +++++++++++++++++++++
dolphinscheduler-ui/src/locales/en_US/project.ts | 2 +
dolphinscheduler-ui/src/locales/zh_CN/project.ts | 2 +
.../task/components/node/fields/use-datax.ts | 41 ++++++-
7 files changed, 344 insertions(+), 8 deletions(-)
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParameters.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParameters.java
index 07c20c3203..b7e51d44d1 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParameters.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParameters.java
@@ -25,13 +25,17 @@ import
org.apache.dolphinscheduler.plugin.task.api.parameters.resource.DataSourc
import
org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper;
import org.apache.dolphinscheduler.spi.enums.Flag;
+import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
import lombok.Data;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
/**
* DataX parameter
*/
@@ -116,7 +120,50 @@ public class DataxParameters extends AbstractParameters {
&& StringUtils.isNotEmpty(sql)
&& StringUtils.isNotEmpty(targetTable);
} else {
- return StringUtils.isNotEmpty(json);
+ // Custom config is valid with either inline json or an attached
resource file that
+ // unambiguously carries the job definition, identified as the
single .json resource
+ // (issue #18389). resourceList is multi-select and also holds
auxiliary files, so a
+ // non-empty list on its own is not enough.
+ return !isInlineJsonAbsent() || getJobDefinitionResource() != null;
+ }
+ }
+
+ /**
+ * When the inline json is absent the job definition must come from an
attached resource file.
+ * resourceList is multi-select and also carries auxiliary files such as
Kerberos keytabs and
+ * xml configs, so the job definition is identified as the single resource
whose name ends with
+ * {@code .json} rather than the first entry in the list (issue #18389).
Returns that resource,
+ * or {@code null} when there is not exactly one json resource, which the
caller treats as a
+ * missing or ambiguous job definition.
+ */
+ public ResourceInfo getJobDefinitionResource() {
+ if (CollectionUtils.isEmpty(resourceList)) {
+ return null;
+ }
+ List<ResourceInfo> jsonResources = resourceList.stream()
+ .filter(Objects::nonNull)
+ .filter(resource ->
StringUtils.endsWithIgnoreCase(resource.getResourceName(), ".json"))
+ .collect(Collectors.toList());
+ return jsonResources.size() == 1 ? jsonResources.get(0) : null;
+ }
+
+ /**
+ * Returns true when the json field carries no usable inline job
definition. The UI
+ * historically stored an empty object placeholder in the json field, so a
blank value
+ * and any semantically empty JSON object (for example {@code {}}, {@code
{ }} or a
+ * formatted multi-line empty object) are all treated as absent (issue
#18389).
+ */
+ public boolean isInlineJsonAbsent() {
+ if (StringUtils.isBlank(json)) {
+ return true;
+ }
+ try {
+ ObjectNode node = JSONUtils.parseObject(json);
+ return node == null || node.isEmpty();
+ } catch (Exception e) {
+ // not parseable as a JSON object, so there is inline content:
downstream
+ // validation reports the malformed definition
+ return false;
}
}
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTask.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTask.java
index e82666d530..021fa783c0 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTask.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/main/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTask.java
@@ -30,8 +30,10 @@ import
org.apache.dolphinscheduler.plugin.task.api.TaskException;
import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext;
import org.apache.dolphinscheduler.plugin.task.api.log.SensitiveDataConverter;
import org.apache.dolphinscheduler.plugin.task.api.model.Property;
+import org.apache.dolphinscheduler.plugin.task.api.model.ResourceInfo;
import org.apache.dolphinscheduler.plugin.task.api.model.TaskResponse;
import
org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters;
+import org.apache.dolphinscheduler.plugin.task.api.resource.ResourceContext;
import
org.apache.dolphinscheduler.plugin.task.api.shell.IShellInterceptorBuilder;
import
org.apache.dolphinscheduler.plugin.task.api.shell.ShellInterceptorBuilderFactory;
import org.apache.dolphinscheduler.plugin.task.api.utils.ParameterUtils;
@@ -168,6 +170,18 @@ public class DataxTask extends AbstractTask {
}
}
+ /**
+ * Reads the DataX job definition from the designated json resource file.
The worker has
+ * already downloaded resources into the execution directory by the time
the task runs.
+ */
+ private String readJsonFromResourceFile(ResourceInfo jobResource) throws
Exception {
+ String resourceFileName = jobResource.getResourceName();
+ ResourceContext resourceContext = taskRequest.getResourceContext();
+ return FileUtils.readFileToString(
+ new
File(resourceContext.getResourceItem(resourceFileName).getResourceAbsolutePathInLocal()),
+ StandardCharsets.UTF_8);
+ }
+
/**
* build datax configuration file
*
@@ -185,7 +199,23 @@ public class DataxTask extends AbstractTask {
}
if (dataXParameters.getCustomConfig() == Flag.YES.ordinal()) {
- json = dataXParameters.getJson().replaceAll("\\r\\n",
System.lineSeparator());
+ // An attached resource file is a valid way to supply the job
definition. Without
+ // this branch the worker downloads the resource but the plugin
runs with the empty
+ // inline json and the job fails (issue #18389). Existing tasks
created through the
+ // UI carry an empty object placeholder, treat it the same as no
inline json.
+ if (dataXParameters.isInlineJsonAbsent()) {
+ // the job definition is the single attached .json resource,
never the first
+ // entry in resourceList, which may be an auxiliary keytab or
xml (issue #18389)
+ ResourceInfo jobResource =
dataXParameters.getJobDefinitionResource();
+ if (jobResource == null) {
+ throw new TaskException(
+ "DataX job definition is missing, provide inline
json or attach exactly one .json resource file");
+ }
+ json = readJsonFromResourceFile(jobResource);
+ } else {
+ json = dataXParameters.getJson();
+ }
+ json = json.replaceAll("\\r\\n", System.lineSeparator());
} else {
ObjectNode job = JSONUtils.createObjectNode();
job.putArray("content").addAll(buildDataxJobContentJson());
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParametersTest.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParametersTest.java
index 736d0aab94..fc161abb23 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParametersTest.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxParametersTest.java
@@ -32,6 +32,108 @@ public class DataxParametersTest {
*/
public static final String JVM_PARAM = " --jvm=\"-Xms%sG -Xmx%sG\" ";
+ @Test
+ public void testCheckParametersWithCustomConfig() {
+ DataxParameters withInlineJson = new DataxParameters();
+ withInlineJson.setCustomConfig(1);
+ withInlineJson.setJson("{\"job\":{}}");
+ Assertions.assertTrue(withInlineJson.checkParameters());
+
+ // a blank json field or any semantically empty JSON object is no
inline
+ // definition: invalid without a resource file, valid with one because
the
+ // resource then carries the job definition (issue #18389)
+ String[] absentJsonVariants = {null, "", " ", "{}", "{ }", "{\n\n}",
" { } "};
+ for (String variant : absentJsonVariants) {
+ DataxParameters withoutResource = new DataxParameters();
+ withoutResource.setCustomConfig(1);
+ withoutResource.setJson(variant);
+ Assertions.assertTrue(withoutResource.isInlineJsonAbsent(),
+ "expected inline json to be absent for: [" + variant +
"]");
+ Assertions.assertFalse(withoutResource.checkParameters(),
+ "expected invalid without resource for json: [" + variant
+ "]");
+
+ DataxParameters withResource = new DataxParameters();
+ withResource.setCustomConfig(1);
+ withResource.setJson(variant);
+ withResource.setResourceList(buildResourceList());
+ Assertions.assertTrue(withResource.checkParameters(),
+ "expected valid with resource for json: [" + variant +
"]");
+ }
+
+ // a non-empty inline definition stays inline even when a resource is
attached
+ DataxParameters inlineWithResource = new DataxParameters();
+ inlineWithResource.setCustomConfig(1);
+ inlineWithResource.setJson("{\"job\":{}}");
+ inlineWithResource.setResourceList(buildResourceList());
+ Assertions.assertFalse(inlineWithResource.isInlineJsonAbsent());
+ Assertions.assertTrue(inlineWithResource.checkParameters());
+
+ // malformed json is not treated as absent, downstream validation
reports it
+ DataxParameters malformed = new DataxParameters();
+ malformed.setCustomConfig(1);
+ malformed.setJson("{invalid");
+ Assertions.assertFalse(malformed.isInlineJsonAbsent());
+
+ DataxParameters withNeither = new DataxParameters();
+ withNeither.setCustomConfig(1);
+ Assertions.assertFalse(withNeither.checkParameters());
+ }
+
+ private List<ResourceInfo> buildResourceList() {
+ ResourceInfo resource = new ResourceInfo();
+ resource.setResourceName("/datax/job.json");
+ List<ResourceInfo> resources = new ArrayList<>();
+ resources.add(resource);
+ return resources;
+ }
+
+ @Test
+ public void testJobDefinitionResourceIsTheSingleJsonResource() {
+ // resourceList is multi-select and also carries auxiliary files, so
the job definition
+ // is identified as the single .json resource, not resourceList.get(0)
(issue #18389)
+
+ // only an auxiliary keytab and no json: no job definition, invalid
+ DataxParameters onlyAuxiliary = new DataxParameters();
+ onlyAuxiliary.setCustomConfig(1);
+ onlyAuxiliary.setResourceList(resources("/datax/hdfs.keytab"));
+ Assertions.assertNull(onlyAuxiliary.getJobDefinitionResource());
+ Assertions.assertFalse(onlyAuxiliary.checkParameters(),
+ "a task carrying only auxiliary resources has no job
definition");
+
+ // a keytab listed before the job file: the .json is chosen, not the
first entry
+ DataxParameters auxiliaryBeforeJob = new DataxParameters();
+ auxiliaryBeforeJob.setCustomConfig(1);
+ auxiliaryBeforeJob.setResourceList(resources("/datax/hdfs.keytab",
"/datax/job.json"));
+ Assertions.assertEquals("/datax/job.json",
+
auxiliaryBeforeJob.getJobDefinitionResource().getResourceName());
+ Assertions.assertTrue(auxiliaryBeforeJob.checkParameters());
+
+ // two json resources are ambiguous: no single job definition, invalid
+ DataxParameters twoJson = new DataxParameters();
+ twoJson.setCustomConfig(1);
+ twoJson.setResourceList(resources("/datax/a.json", "/datax/b.json"));
+ Assertions.assertNull(twoJson.getJobDefinitionResource());
+ Assertions.assertFalse(twoJson.checkParameters());
+
+ // exactly one json resource is the job definition
+ DataxParameters singleJson = new DataxParameters();
+ singleJson.setCustomConfig(1);
+ singleJson.setResourceList(resources("/datax/job.json"));
+ Assertions.assertEquals("/datax/job.json",
+ singleJson.getJobDefinitionResource().getResourceName());
+ Assertions.assertTrue(singleJson.checkParameters());
+ }
+
+ private List<ResourceInfo> resources(String... names) {
+ List<ResourceInfo> list = new ArrayList<>();
+ for (String name : names) {
+ ResourceInfo resource = new ResourceInfo();
+ resource.setResourceName(name);
+ list.add(resource);
+ }
+ return list;
+ }
+
@Test
public void testLoadJvmEnv() {
diff --git
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTaskTest.java
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTaskTest.java
index 17687e15c1..5d75150edf 100644
---
a/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTaskTest.java
+++
b/dolphinscheduler-task-plugin/dolphinscheduler-task-datax/src/test/java/org/apache/dolphinscheduler/plugin/task/datax/DataxTaskTest.java
@@ -37,8 +37,10 @@ import
org.apache.dolphinscheduler.plugin.task.api.enums.Direct;
import org.apache.dolphinscheduler.plugin.task.api.enums.TaskRunStatus;
import org.apache.dolphinscheduler.plugin.task.api.model.ApplicationInfo;
import org.apache.dolphinscheduler.plugin.task.api.model.Property;
+import org.apache.dolphinscheduler.plugin.task.api.model.ResourceInfo;
import org.apache.dolphinscheduler.plugin.task.api.model.TaskResponse;
import
org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper;
+import org.apache.dolphinscheduler.plugin.task.api.resource.ResourceContext;
import org.apache.dolphinscheduler.spi.datasource.BaseConnectionParam;
import org.apache.dolphinscheduler.spi.enums.DbType;
@@ -271,6 +273,128 @@ public class DataxTaskTest {
return paramsMap;
}
+ @Test
+ public void testCustomConfigReadsJobDefinitionFromResourceFile() throws
Exception {
+ // a real resource file carrying the job definition, with a formatted
empty object
+ // placeholder inline (the semantic-absence rule, not a literal "{}"
compare)
+ String resourceJson =
"{\"job\":{\"content\":[{\"reader\":{\"name\":\"mysqlreader\"}}]}}";
+ File resourceFile = File.createTempFile("datax-job", ".json");
+ resourceFile.deleteOnExit();
+ java.nio.file.Files.write(resourceFile.toPath(),
+
resourceJson.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+
+ DataxParameters parameters = new DataxParameters();
+ parameters.setCustomConfig(1);
+ parameters.setJson("{\n \n}");
+ parameters.setXms(1);
+ parameters.setXmx(1);
+ ResourceInfo resourceInfo = new ResourceInfo();
+ resourceInfo.setResourceName("/datax/job.json");
+
parameters.setResourceList(java.util.Collections.singletonList(resourceInfo));
+
+ TaskExecutionContext taskExecutionContext =
buildTestTaskExecutionContext();
+ // own app id so the generated file cannot collide with other tests'
job files
+ taskExecutionContext.setTaskAppId("app-id-resource");
+ taskExecutionContext.setPrepareParamsMap(null);
+ taskExecutionContext.setTaskParams(JSONUtils.toJsonString(parameters));
+ ResourceContext resourceContext = new ResourceContext();
+ resourceContext.addResourceItem(ResourceContext.ResourceItem.builder()
+ .resourceAbsolutePathInStorage("/datax/job.json")
+ .resourceAbsolutePathInLocal(resourceFile.getAbsolutePath())
+ .build());
+ taskExecutionContext.setResourceContext(resourceContext);
+
+ DataxTask dataxTask = new DataxTask(taskExecutionContext);
+ dataxTask.init();
+
+ ShellCommandExecutor shellCommandExecutor =
mock(ShellCommandExecutor.class);
+ Field shellCommandExecutorFiled =
DataxTask.class.getDeclaredField("shellCommandExecutor");
+ shellCommandExecutorFiled.setAccessible(true);
+ shellCommandExecutorFiled.set(dataxTask, shellCommandExecutor);
+
+ TaskResponse taskResponse = new TaskResponse();
+ taskResponse.setStatus(TaskRunStatus.SUCCESS);
+ taskResponse.setExitStatusCode(0);
+ taskResponse.setProcessId(1);
+ when(shellCommandExecutor.run(any(),
eq(taskCallBack))).thenReturn(taskResponse);
+
+ dataxTask.handle(taskCallBack);
+ Assertions.assertEquals(0, dataxTask.getExitStatusCode());
+
+ // the generated job file must carry the resource content, not the
"{}" placeholder
+ File jsonFile = new File("/tmp/execution/app-id-resource_job.json");
+ String generated =
FileUtils.readFile2Str(Files.newInputStream(jsonFile.toPath()));
+ Assertions.assertTrue(generated.contains("mysqlreader"),
+ "generated job file should contain the resource file
definition, was: " + generated);
+ Assertions.assertTrue(jsonFile.delete());
+ }
+
+ @Test
+ public void
testCustomConfigReadsJobFromJsonResourceNotFirstAuxiliaryResource() throws
Exception {
+ // resourceList carries a keytab BEFORE the job file. The worker must
read the .json job
+ // definition, not resourceList.get(0) which is the keytab (issue
#18389, review by SbloodyS)
+ String jobJson =
"{\"job\":{\"content\":[{\"reader\":{\"name\":\"mysqlreader\"}}]}}";
+ File jobFile = File.createTempFile("datax-job", ".json");
+ jobFile.deleteOnExit();
+ java.nio.file.Files.write(jobFile.toPath(),
+ jobJson.getBytes(java.nio.charset.StandardCharsets.UTF_8));
+ File keytabFile = File.createTempFile("hdfs", ".keytab");
+ keytabFile.deleteOnExit();
+ java.nio.file.Files.write(keytabFile.toPath(),
+
"keytab-binary-not-json".getBytes(java.nio.charset.StandardCharsets.UTF_8));
+
+ DataxParameters parameters = new DataxParameters();
+ parameters.setCustomConfig(1);
+ parameters.setJson("{}");
+ parameters.setXms(1);
+ parameters.setXmx(1);
+ ResourceInfo keytab = new ResourceInfo();
+ keytab.setResourceName("/datax/hdfs.keytab");
+ ResourceInfo job = new ResourceInfo();
+ job.setResourceName("/datax/job.json");
+ parameters.setResourceList(java.util.Arrays.asList(keytab, job));
+
+ TaskExecutionContext taskExecutionContext =
buildTestTaskExecutionContext();
+ taskExecutionContext.setTaskAppId("app-id-multi-resource");
+ taskExecutionContext.setPrepareParamsMap(null);
+ taskExecutionContext.setTaskParams(JSONUtils.toJsonString(parameters));
+ ResourceContext resourceContext = new ResourceContext();
+ resourceContext.addResourceItem(ResourceContext.ResourceItem.builder()
+ .resourceAbsolutePathInStorage("/datax/hdfs.keytab")
+ .resourceAbsolutePathInLocal(keytabFile.getAbsolutePath())
+ .build());
+ resourceContext.addResourceItem(ResourceContext.ResourceItem.builder()
+ .resourceAbsolutePathInStorage("/datax/job.json")
+ .resourceAbsolutePathInLocal(jobFile.getAbsolutePath())
+ .build());
+ taskExecutionContext.setResourceContext(resourceContext);
+
+ DataxTask dataxTask = new DataxTask(taskExecutionContext);
+ dataxTask.init();
+
+ ShellCommandExecutor shellCommandExecutor =
mock(ShellCommandExecutor.class);
+ Field shellCommandExecutorFiled =
DataxTask.class.getDeclaredField("shellCommandExecutor");
+ shellCommandExecutorFiled.setAccessible(true);
+ shellCommandExecutorFiled.set(dataxTask, shellCommandExecutor);
+
+ TaskResponse taskResponse = new TaskResponse();
+ taskResponse.setStatus(TaskRunStatus.SUCCESS);
+ taskResponse.setExitStatusCode(0);
+ taskResponse.setProcessId(1);
+ when(shellCommandExecutor.run(any(),
eq(taskCallBack))).thenReturn(taskResponse);
+
+ dataxTask.handle(taskCallBack);
+ Assertions.assertEquals(0, dataxTask.getExitStatusCode());
+
+ File jsonFile = new
File("/tmp/execution/app-id-multi-resource_job.json");
+ String generated =
FileUtils.readFile2Str(Files.newInputStream(jsonFile.toPath()));
+ Assertions.assertTrue(generated.contains("mysqlreader"),
+ "generated job file should carry the .json resource content,
was: " + generated);
+ Assertions.assertFalse(generated.contains("keytab-binary-not-json"),
+ "generated job file must not read the auxiliary keytab as the
job definition");
+ Assertions.assertTrue(jsonFile.delete());
+ }
+
private TaskExecutionContext buildTestTaskExecutionContext() {
TaskExecutionContext taskExecutionContext = new TaskExecutionContext();
taskExecutionContext.setTaskAppId("app-id");
diff --git a/dolphinscheduler-ui/src/locales/en_US/project.ts
b/dolphinscheduler-ui/src/locales/en_US/project.ts
index 534c11915b..cc88f08190 100644
--- a/dolphinscheduler-ui/src/locales/en_US/project.ts
+++ b/dolphinscheduler-ui/src/locales/en_US/project.ts
@@ -645,6 +645,8 @@ export default {
or: 'or',
datax_custom_template: 'Custom Template',
datax_json_template: 'JSON',
+ datax_custom_json_resource_tips:
+ 'When the custom JSON is empty, attach exactly one .json resource file
that carries the DataX job definition.',
datax_target_datasource_type: 'Target Datasource Types',
datax_target_database: 'Target Database',
datax_target_table: 'Target Table',
diff --git a/dolphinscheduler-ui/src/locales/zh_CN/project.ts
b/dolphinscheduler-ui/src/locales/zh_CN/project.ts
index 429fd4990b..24552393a4 100644
--- a/dolphinscheduler-ui/src/locales/zh_CN/project.ts
+++ b/dolphinscheduler-ui/src/locales/zh_CN/project.ts
@@ -626,6 +626,8 @@ export default {
or: '或',
datax_custom_template: '自定义模板',
datax_json_template: 'JSON',
+ datax_custom_json_resource_tips:
+ '当自定义 JSON 为空时, 需要且仅需要附加一个携带 DataX 任务定义的 .json 资源文件。',
datax_target_datasource_type: '目标源类型',
datax_target_database: '目标源实例',
datax_target_table: '目标表',
diff --git
a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datax.ts
b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datax.ts
index 8b5c3ae934..45698908bd 100644
---
a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datax.ts
+++
b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datax.ts
@@ -197,18 +197,47 @@ export function useDataX(model: { [field: string]: any
}): IJsonItem[] {
span: jsonEditorSpan,
validate: {
trigger: ['input', 'trigger'],
- required: true,
+ required: false,
validator() {
- if (
- model.json === '' ||
+ // When the inline json is absent the job definition must come from
exactly one
+ // attached .json resource. resourceList is multi-select and also
carries auxiliary
+ // files such as keytabs and xml, so the same rule as the backend
+ // DataxParameters.getJobDefinitionResource applies here (issue
#18389).
+ const resourceList = (model.resourceList as string[]) || []
+ const hasSingleJsonResource =
+ resourceList.filter(
+ (fullName) =>
+ typeof fullName === 'string' &&
+ fullName.toLowerCase().endsWith('.json')
+ ).length === 1
+ // Treat a whitespace-only value as absent too, matching the backend
+ // DataxParameters.isInlineJsonAbsent which uses
StringUtils.isBlank. Otherwise a
+ // blank inline json would reach utils.isJson below and be rejected
even when exactly
+ // one valid .json resource is attached, a configuration the worker
would have accepted.
+ const inlineJsonAbsent =
model.json === undefined ||
- model.json === null
- ) {
- return new Error(t('project.node.sql_empty_tips'))
+ model.json === null ||
+ (model.json as string).trim() === ''
+ if (inlineJsonAbsent) {
+ return hasSingleJsonResource
+ ? undefined
+ : new Error(t('project.node.datax_custom_json_resource_tips'))
}
if (!utils.isJson(model.json)) {
return new Error(t('project.node.json_format_tips'))
}
+ // A semantically empty object ({}, { }, formatted) is the
historical UI
+ // placeholder and does not count as an inline definition. Same rule
as
+ // DataxParameters.isInlineJsonAbsent on the backend.
+ const parsed = JSON.parse(model.json)
+ const isEmptyObject =
+ parsed !== null &&
+ typeof parsed === 'object' &&
+ !Array.isArray(parsed) &&
+ Object.keys(parsed).length === 0
+ if (isEmptyObject && !hasSingleJsonResource) {
+ return new Error(t('project.node.datax_custom_json_resource_tips'))
+ }
}
}
},