From ff99a7002f335983f196d2339d2a964ec3a25ccf Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Fri, 28 Aug 2026 15:31:04 +0800 Subject: [PATCH 1/2] feat: add batchCreateItems, batchUpdateItems, batchDeleteItems item contracts Expose three new Item Management operations for submitting a list of namespace items to create, update, or delete in one call: - POST .../items/batch-create (batchCreateItems) - PUT .../items/batch-update (batchUpdateItems) - POST .../items/batch-delete (batchDeleteItems) Bump version to 0.3.11. --- CHANGELOG.md | 6 + apollo-openapi.yaml | 201 +++++++++++++++++++++++++++++- tests/test_item_batch_contract.py | 97 ++++++++++++++ 3 files changed, 303 insertions(+), 1 deletion(-) create mode 100644 tests/test_item_batch_contract.py diff --git a/CHANGELOG.md b/CHANGELOG.md index bb8a3d4..f543719 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.11] - 2026-08-28 + +### Added + +- Add `batchCreateItems` (`POST .../items/batch-create`), `batchUpdateItems` (`PUT .../items/batch-update`), and `batchDeleteItems` (`POST .../items/batch-delete`) APIs for submitting a list of namespace items to create, update, or delete in one call. + ## [0.3.9] - 2026-06-10 ### Added diff --git a/apollo-openapi.yaml b/apollo-openapi.yaml index bed0f6c..2e6ae93 100644 --- a/apollo-openapi.yaml +++ b/apollo-openapi.yaml @@ -18,7 +18,7 @@ info:
curl -X GET "http://localhost:8070/openapi/v1/apps" \
     -H "Authorization: your_token_here"
- version: 0.3.9 + version: 0.3.11 security: - ApiKeyAuth: [] tags: @@ -945,6 +945,205 @@ paths: schema: $ref: '#/components/schemas/ExceptionResponse' headers: {} + /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/batch-create: + post: + summary: 批量创建配置项 (new added) + operationId: batchCreateItems + deprecated: false + description: >- + POST + /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/batch-create + tags: + - Item Management + parameters: + - name: appId + in: path + description: '应用ID' + required: true + schema: + type: string + - name: env + in: path + description: '环境标识' + required: true + schema: + type: string + - name: clusterName + in: path + description: '集群名称' + required: true + schema: + type: string + - name: namespaceName + in: path + description: '命名空间名称' + required: true + schema: + type: string + - name: operator + in: query + description: '操作人用户名' + required: false + schema: + type: string + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OpenItemDTO' + required: true + responses: + '200': + description: '批量创建配置项成功' + '400': + description: '请求参数错误' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + '403': + description: '权限不足' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/batch-update: + put: + summary: 批量修改配置项 (new added) + operationId: batchUpdateItems + deprecated: false + description: >- + PUT + /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/batch-update + tags: + - Item Management + parameters: + - name: appId + in: path + description: '应用ID' + required: true + schema: + type: string + - name: env + in: path + description: '环境标识' + required: true + schema: + type: string + - name: clusterName + in: path + description: '集群名称' + required: true + schema: + type: string + - name: namespaceName + in: path + description: '命名空间名称' + required: true + schema: + type: string + - name: operator + in: query + description: '操作人用户名' + required: false + schema: + type: string + requestBody: + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/OpenItemDTO' + required: true + responses: + '200': + description: '批量修改配置项成功' + '400': + description: '请求参数错误' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + '403': + description: '权限不足' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + '404': + description: '配置项不存在' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/batch-delete: + post: + summary: 批量删除配置项 (new added) + operationId: batchDeleteItems + deprecated: false + description: >- + POST + /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/items/batch-delete + tags: + - Item Management + parameters: + - name: appId + in: path + description: '应用ID' + required: true + schema: + type: string + - name: env + in: path + description: '环境标识' + required: true + schema: + type: string + - name: clusterName + in: path + description: '集群名称' + required: true + schema: + type: string + - name: namespaceName + in: path + description: '命名空间名称' + required: true + schema: + type: string + - name: operator + in: query + description: '操作人用户名' + required: false + schema: + type: string + requestBody: + content: + application/json: + schema: + type: array + items: + type: string + description: 待删除的配置项 key 列表 + required: true + responses: + '200': + description: '批量删除配置项成功' + '403': + description: '权限不足' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' + '404': + description: '配置项不存在' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}/namespaces/{namespaceName}/encodedItems/{key}: get: summary: 通过查询参数获取配置项(支持编码的key) (original openapi) diff --git a/tests/test_item_batch_contract.py b/tests/test_item_batch_contract.py new file mode 100644 index 0000000..28103c2 --- /dev/null +++ b/tests/test_item_batch_contract.py @@ -0,0 +1,97 @@ +import unittest +from pathlib import Path + +import yaml + + +SPEC_FILES = ( + "apollo-openapi.yaml", +) + +BASE_PATH = ( + "/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}" + "/namespaces/{namespaceName}/items" +) + + +class ItemBatchContractTest(unittest.TestCase): + + def setUp(self): + self.repo_root = Path(__file__).resolve().parents[1] + + def _load_spec(self, spec_file): + return yaml.safe_load((self.repo_root / spec_file).read_text(encoding="utf-8")) + + def test_item_batch_paths_use_generated_contract(self): + for spec_file in SPEC_FILES: + spec = self._load_spec(spec_file) + + with self.subTest(spec=spec_file): + tag_names = {tag["name"] for tag in spec["tags"]} + self.assertIn("Item Management", tag_names) + + expected_operations = { + f"{BASE_PATH}/batch-create": ("post", "batchCreateItems"), + f"{BASE_PATH}/batch-update": ("put", "batchUpdateItems"), + f"{BASE_PATH}/batch-delete": ("post", "batchDeleteItems"), + } + for path, (method, operation_id) in expected_operations.items(): + operation = spec["paths"][path][method] + self.assertEqual(operation_id, operation["operationId"]) + self.assertEqual(["Item Management"], operation["tags"]) + self.assertEqual( + "#/components/schemas/ExceptionResponse", + operation["responses"]["403"]["content"]["application/json"]["schema"]["$ref"], + ) + + parameter_names = {parameter["name"] for parameter in operation["parameters"]} + self.assertEqual( + {"appId", "env", "clusterName", "namespaceName", "operator"}, + parameter_names, + ) + operator_parameter = next( + parameter for parameter in operation["parameters"] + if parameter["name"] == "operator") + self.assertFalse(operator_parameter["required"]) + + def test_batch_create_and_update_accept_item_list(self): + for spec_file in SPEC_FILES: + spec = self._load_spec(spec_file) + + with self.subTest(spec=spec_file): + for path, method in ( + (f"{BASE_PATH}/batch-create", "post"), + (f"{BASE_PATH}/batch-update", "put"), + ): + operation = spec["paths"][path][method] + request_schema = operation["requestBody"]["content"]["application/json"]["schema"] + self.assertEqual("array", request_schema["type"]) + self.assertEqual( + "#/components/schemas/OpenItemDTO", + request_schema["items"]["$ref"], + ) + + update_operation = spec["paths"][f"{BASE_PATH}/batch-update"]["put"] + self.assertEqual( + "#/components/schemas/ExceptionResponse", + update_operation["responses"]["404"]["content"]["application/json"]["schema"][ + "$ref"], + ) + + def test_batch_delete_accepts_key_list(self): + for spec_file in SPEC_FILES: + spec = self._load_spec(spec_file) + + with self.subTest(spec=spec_file): + operation = spec["paths"][f"{BASE_PATH}/batch-delete"]["post"] + request_schema = operation["requestBody"]["content"]["application/json"]["schema"] + self.assertEqual("array", request_schema["type"]) + self.assertEqual("string", request_schema["items"]["type"]) + self.assertEqual( + "#/components/schemas/ExceptionResponse", + operation["responses"]["404"]["content"]["application/json"]["schema"]["$ref"], + ) + + +if __name__ == "__main__": + unittest.main() From 62d3b987d3da8e2ee1e6de8639f0b345671abc6b Mon Sep 17 00:00:00 2001 From: "shalk(xiao kun)" Date: Fri, 4 Sep 2026 10:57:37 +0800 Subject: [PATCH 2/2] fix: enforce batch item request validation in the contract - Add minItems: 1 to the batch-create/update/delete request arrays - Add minLength: 1 to batch-delete's key items - Document 400 with ExceptionResponse for batchDeleteItems - Extend tests/test_item_batch_contract.py to cover these constraints Addresses nobodyiam's review on PR apolloconfig/apollo-openapi#37: the companion apolloconfig/apollo#5665 controller already rejects these inputs with HTTP 400, so the contract should advertise the same constraints instead of leaving them undocumented. Co-Authored-By: Claude Sonnet 5 --- apollo-openapi.yaml | 10 ++++++++++ tests/test_item_batch_contract.py | 7 +++++++ 2 files changed, 17 insertions(+) diff --git a/apollo-openapi.yaml b/apollo-openapi.yaml index 2e6ae93..45f420b 100644 --- a/apollo-openapi.yaml +++ b/apollo-openapi.yaml @@ -991,6 +991,7 @@ paths: application/json: schema: type: array + minItems: 1 items: $ref: '#/components/schemas/OpenItemDTO' required: true @@ -1055,6 +1056,7 @@ paths: application/json: schema: type: array + minItems: 1 items: $ref: '#/components/schemas/OpenItemDTO' required: true @@ -1125,13 +1127,21 @@ paths: application/json: schema: type: array + minItems: 1 items: type: string + minLength: 1 description: 待删除的配置项 key 列表 required: true responses: '200': description: '批量删除配置项成功' + '400': + description: '请求参数错误' + content: + application/json: + schema: + $ref: '#/components/schemas/ExceptionResponse' '403': description: '权限不足' content: diff --git a/tests/test_item_batch_contract.py b/tests/test_item_batch_contract.py index 28103c2..7ffa42c 100644 --- a/tests/test_item_batch_contract.py +++ b/tests/test_item_batch_contract.py @@ -66,6 +66,7 @@ def test_batch_create_and_update_accept_item_list(self): operation = spec["paths"][path][method] request_schema = operation["requestBody"]["content"]["application/json"]["schema"] self.assertEqual("array", request_schema["type"]) + self.assertEqual(1, request_schema["minItems"]) self.assertEqual( "#/components/schemas/OpenItemDTO", request_schema["items"]["$ref"], @@ -86,7 +87,13 @@ def test_batch_delete_accepts_key_list(self): operation = spec["paths"][f"{BASE_PATH}/batch-delete"]["post"] request_schema = operation["requestBody"]["content"]["application/json"]["schema"] self.assertEqual("array", request_schema["type"]) + self.assertEqual(1, request_schema["minItems"]) self.assertEqual("string", request_schema["items"]["type"]) + self.assertEqual(1, request_schema["items"]["minLength"]) + self.assertEqual( + "#/components/schemas/ExceptionResponse", + operation["responses"]["400"]["content"]["application/json"]["schema"]["$ref"], + ) self.assertEqual( "#/components/schemas/ExceptionResponse", operation["responses"]["404"]["content"]["application/json"]["schema"]["$ref"],