This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new d2f0f762c fix(settings): align the data-source, model and ACL
contracts (#6011)
d2f0f762c is described below
commit d2f0f762cc8e503fe314168e7cb79ef6bf40fab1
Author: 烤化の初雪 <[email protected]>
AuthorDate: Mon Oct 12 16:13:20 2026 +0800
fix(settings): align the data-source, model and ACL contracts (#6011)
Three implementations that disagreed with a contract the repository already
publishes — the API spec, the server's curated model list, and the
duplicate-key handling used by every sibling path in the same ACL repository.
- #6011 `SettingsController.java:83` bound only `@RequestParam(required =
false) String key` for `POST /api/settings/datasources/delete`, while
`docs/api-spec.md` §14.7 (`:2362-2366`) documents `key` as a required **body**
field, so a spec-conformant client sent `key = null` and
`SettingsService.deleteDataSource` (`:276-283`) answered 400 "Data source key
is required" having deleted nothing → the handler also accepts
`@RequestBody(required = false) DataSourceKeyDTO`, a new `@Data` DTO [...]
- #6013 `web/src/pages/studio/llmModelOptions.ts:31-40` had drifted from
the server's curated `LlmConfigService.PROVIDER_MODELS` — `tongyi` listed 7 of
the 12 ids at `LlmConfigService.java:69-81` and `ollama` listed `gemma2`, which
`:82-85` does not serve → the fallback mirrors both lists in server order with
a comment naming `PROVIDER_MODELS` as the single source of truth, so
`AiAssistantTab.tsx:130` (provider switch) and `:189-190` (first load, or a
failed `GET /llm/models`) stop hi [...]
- #6015 `MybatisPlusAclRepository.createAndUpdatePlainAccessConfig` always
executes `entity.setUsername(config.getAccessKey())` at `:257` and then a bare
`userMapper.updateById(entity)` at `:270`, so when a stored row's username had
been renamed away from its access key (`replaceUser` supports renaming,
`:164-176`) and another row already owned `username = accessKey`, the
`uk_username` violation escaped as a 500 "Internal Server Error" and the
`@Transactional` method (`:235-236`) roll [...]
Tests: one case each, all mutation-sensitive —
`SettingsControllerTest.deleteDataSourceShouldAcceptTheDocumentedKeyBodyTest`
(23 → 24) pins `isOk()` plus `verify(settingsService).deleteDataSource("ds-1")`
alongside the existing query-parameter and missing-key cases,
`LlmSettings.test.ts` (4 → 5) pins both providers' ordered lists with
`toEqual`, and `MybatisPlusAclRepositoryTest` (25 → 26) pins the 409, its
message and `verifyNoInteractions(ruleMapper)`, filling the one duplicate-key
[...]
Folded: #6013, #6015 were closed as folded into this commit (same author).
---
.../instance/acl/MybatisPlusAclRepository.java | 10 ++++++++-
.../rocketmq/studio/settings/DataSourceKeyDTO.java | 26 ++++++++++++++++++++++
.../studio/settings/SettingsController.java | 8 +++++--
.../instance/acl/MybatisPlusAclRepositoryTest.java | 19 ++++++++++++++++
.../studio/settings/SettingsControllerTest.java | 15 +++++++++++++
web/src/pages/studio/__tests__/LlmSettings.test.ts | 25 +++++++++++++++++++++
web/src/pages/studio/llmModelOptions.ts | 14 +++++++++++-
7 files changed, 113 insertions(+), 4 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
index a848bdbcf..5f751e992 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
@@ -267,7 +267,15 @@ public class MybatisPlusAclRepository implements
AclRepository {
entity.setWhiteRemoteAddress(normalizeWhiteRemoteAddress(config.getWhiteRemoteAddress()));
entity.setGmtModified(LocalDateTime.now());
if (existing != null) {
- userMapper.updateById(entity);
+ try {
+ userMapper.updateById(entity);
+ } catch (DuplicateKeyException exception) {
+ // The update rewrites username to the accessKey, so a row
that was renamed through
+ // the users form (or created with that name concurrently)
hits uk_username here -
+ // the same collision the sibling write paths already
translate, and without this the
+ // caller saw a generic 500 "Internal Server Error" for a
client mistake.
+ throw aclUserConflict(config.getAccessKey());
+ }
if (entity.getWhiteRemoteAddress() == null) {
// Clearing the whitelist must persist as a null column;
updateById skips null fields.
clearColumn(userMapper, entity.getId(),
"white_remote_address");
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/settings/DataSourceKeyDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/settings/DataSourceKeyDTO.java
new file mode 100644
index 000000000..2c35a060f
--- /dev/null
+++
b/server/src/main/java/org/apache/rocketmq/studio/settings/DataSourceKeyDTO.java
@@ -0,0 +1,26 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.settings;
+
+import lombok.Data;
+
+/** The documented request body of the data-source deletion, which also
accepts the query parameter. */
+@Data
+public class DataSourceKeyDTO {
+
+ private String key;
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsController.java
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsController.java
index d80d588aa..f8f01c9b3 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsController.java
@@ -80,8 +80,12 @@ public class SettingsController {
}
@PostMapping("/datasources/delete")
- public Result<Void> deleteDataSource(@RequestParam(required = false)
String key) {
- settingsService.deleteDataSource(key);
+ public Result<Void> deleteDataSource(@RequestParam(required = false)
String key,
+ @RequestBody(required = false)
DataSourceKeyDTO request) {
+ // The API spec documents a {"key": ...} body while the console sends
the query parameter;
+ // binding only the parameter made the documented request a 400 "Data
source key is
+ // required" and deleted nothing.
+ settingsService.deleteDataSource(key != null ? key : request == null ?
null : request.getKey());
return Result.ok();
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
index b78d4e340..6b5b984d9 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
@@ -418,6 +418,25 @@ class MybatisPlusAclRepositoryTest {
verifyNoInteractions(ruleMapper);
}
+ @Test
+ void updateShouldReportARenameOntoAnExistingUsernameAsAConflictTest() {
+ RmqAclUser existing = userEntity(1L, "svc-c",
CredentialUtils.encodeBase64("kept-secret"));
+ existing.setAccessKey("K");
+
when(userMapper.selectList(any(QueryWrapper.class))).thenReturn(List.of(existing));
+ // Another row already owns username=K (uk_username), e.g. a user
created after this account
+ // was renamed away from it.
+ when(userMapper.updateById(any(RmqAclUser.class)))
+ .thenThrow(new
org.springframework.dao.DuplicateKeyException("duplicate"));
+
+ PlainAccessConfigVO config =
PlainAccessConfigVO.builder().accessKey("K").build();
+
+ assertThatThrownBy(() ->
repository.createAndUpdatePlainAccessConfig(config))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("ACL user already exists: K")
+ .satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(409));
+ org.mockito.Mockito.verifyNoInteractions(ruleMapper);
+ }
+
@Test
void updateWithBlankSecretShouldKeepStoredSecret() {
RmqAclUser existing = userEntity(1L, "svc-x",
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsControllerTest.java
index 4067d4f2d..74258f82b 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsControllerTest.java
@@ -444,6 +444,21 @@ class SettingsControllerTest extends WebMvcAuthTestSupport
{
verify(settingsService).deleteDataSource("ds-1");
}
+ @Test
+ void deleteDataSourceShouldAcceptTheDocumentedKeyBodyTest() throws
Exception {
+ doNothing().when(settingsService).deleteDataSource("ds-1");
+
+ // The API spec (§14.7) documents a {"key": ...} body; binding only
the query parameter made
+ // a doc-conformant client's request a 400 that deleted nothing.
+ mockMvc.perform(post("/api/settings/datasources/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{\"key\":\"ds-1\"}"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code", is(200)));
+
+ verify(settingsService).deleteDataSource("ds-1");
+ }
+
@Test
void deleteDataSourceShouldRejectMissingKeyTest() throws Exception {
doThrow(new BusinessException(400, "Data source key is required"))
diff --git a/web/src/pages/studio/__tests__/LlmSettings.test.ts
b/web/src/pages/studio/__tests__/LlmSettings.test.ts
index a00b44867..490d0b660 100644
--- a/web/src/pages/studio/__tests__/LlmSettings.test.ts
+++ b/web/src/pages/studio/__tests__/LlmSettings.test.ts
@@ -56,6 +56,31 @@ describe('LlmSettingsPage', () => {
]);
});
+ it('mirrors the server curated model list per provider', () => {
+ // LlmConfigService.PROVIDER_MODELS is the source of truth (GET
/llm/models and the home page
+ // picker). The fallback is what the settings dialog shows when that
endpoint fails or the
+ // provider is re-selected, so a shorter list hides models the gateway
serves.
+ expect(fallbackModelOptions('tongyi').map((option) =>
option.value)).toEqual([
+ 'qwen3.8-max',
+ 'qwen3.7-max',
+ 'qwen3.7-plus',
+ 'gpt-5',
+ 'gpt-5.1',
+ 'claude-fable-5',
+ 'claude-opus-5',
+ 'claude-sonnet-5',
+ 'deepseek-v4-pro',
+ 'deepseek-v4-flash',
+ 'MiniMax-M2.5',
+ 'glm-5.2',
+ ]);
+ expect(fallbackModelOptions('ollama').map((option) =>
option.value)).toEqual([
+ 'llama3',
+ 'mistral',
+ 'qwen2.5',
+ ]);
+ });
+
it('falls back to the current model for unknown providers', () => {
expect(fallbackModelOptions('custom', 'custom-model')).toEqual([
{ value: 'custom-model', label: 'custom-model' },
diff --git a/web/src/pages/studio/llmModelOptions.ts
b/web/src/pages/studio/llmModelOptions.ts
index ba94e032f..b81c8d428 100644
--- a/web/src/pages/studio/llmModelOptions.ts
+++ b/web/src/pages/studio/llmModelOptions.ts
@@ -28,16 +28,28 @@ export const FALLBACK_MODELS: Record<string, string[]> = {
'claude-haiku-4-5',
],
deepseek: ['deepseek-chat', 'deepseek-reasoner'],
+ /**
+ * Mirrors `LlmConfigService.PROVIDER_MODELS["tongyi"]` (what `GET
/llm/models` answers and what
+ * the home page offers). The list is used whenever the endpoint fails or
the provider is
+ * re-selected, so a shorter copy hid five of the twelve models the gateway
serves.
+ */
tongyi: [
'qwen3.8-max',
'qwen3.7-max',
'qwen3.7-plus',
+ 'gpt-5',
+ 'gpt-5.1',
+ 'claude-fable-5',
+ 'claude-opus-5',
+ 'claude-sonnet-5',
'deepseek-v4-pro',
'deepseek-v4-flash',
'MiniMax-M2.5',
'glm-5.2',
],
- ollama: ['llama3', 'mistral', 'gemma2', 'qwen2.5'],
+ // Mirrors the server's list for the same reason (it offered gemma2, which
the gateway does not
+ // serve).
+ ollama: ['llama3', 'mistral', 'qwen2.5'],
bedrock: [
'anthropic.claude-fable-5',
'anthropic.claude-opus-5',