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',

Reply via email to